When building a homepage in Vue, it’s common to split the UI into multiple components. Some of them are purely presentational, while others fetch data from APIs.
Here’s the problem: if one of those components uses an await during its setup, Vue will wait for it before rendering the parent. That means a single API call can block the entire page from appearing to the user.
That’s not what we want. A modern web app should feel snappy and responsive, even when waiting for data.
Vue 3 gives us the perfect tool for this: <Suspense>.
๐ The Starting Point
Let’s look at a simplified index.vue homepage:
<template>
<div>
<Component1 :perServingPrice="data.perServingPrice" />
<Component2 :perServingPrice="data.perServingPrice" />
<Component3 :landingContentKey="landingContentKey" />
<Component4 :perServingPrice="data.perServingPrice" />
<Component5 :landingContentKey="landingContentKey" />
<Component6 :recipes="data.recipes" />
<Component7 :landingContentKey="landingContentKey" />
<Component8 :recipes="data.recipes" />
</div>
</template>
<script setup lang="ts">
import Component1 from '@/components/HomePage/Component1.vue'
import Component2 from '@/components/HomePage/Component2.vue'
import Component3 from '@/components/HomePage/Component3.vue'
import Component4 from '@/components/HomePage/Component4.vue'
import Component5 from '@/components/HomePage/Component5.vue'
import Component6 from '@/components/HomePage/Component6.vue'
import Component7 from '@/components/HomePage/Component7.vue'
import Component8 from '@/components/HomePage/Component8.vue'
const data = {
perServingPrice: 10,
recipes: [],
}
const landingContentKey = 'homepage'
</script>
Now imagine:
Component2fetches special offers.Component6fetches recipe data.Component8fetches trending dishes.
If those API calls are written like this inside a child component:
<script setup lang="ts">
const response = await fetch('/api/recipes')
const recipes = await response.json()
</script>
โก๏ธ Vue will not render the parent index.vue until this await is finished. That means the entire page waits, even though other components (like Component1 and Component3) donโt need that data at all.
โณ Why Blocking Is a Problem
Letโs simulate the render timeline without <Suspense>:
- At
t=0s: Page requested. - At
t=0.3s: HTML + JS bundles load. - At
t=0.4s: Component2 makes an API request. - At
t=0.8s: Component6 makes an API request. - At
t=1.2s: Component8 makes an API request. - At
t=2.0s: All API responses return โ finally the page renders.
The user stares at a blank page until everything resolves. ๐ฉ
๐ฏ Enter <Suspense>
The <Suspense> component lets you wrap child components that might suspend (pause) while awaiting data. Instead of blocking the whole page, Vue shows:
- The rest of the parent page (immediately).
- A fallback placeholder for the async child until it’s ready.
๐ Example: Wrapping Component6
<Suspense>
<template #default>
<Component6 :recipes="data.recipes" />
</template>
<template #fallback>
<div class="skeleton">Loading recipes...</div>
</template>
</Suspense>
Here’s what happens now:
- At
t=0.4s: The page renders. <Component6>isnโt ready yet, so Vue shows the fallback (Loading recipes...).- At
t=2.0s: Recipes arrive โ Vue automatically replaces the fallback with the actual component.
Result: The page is usable instantly. โ
๐ Applying Suspense to Multiple Components
We can selectively wrap only the async components:
<template>
<div>
<Component1 :perServingPrice="data.perServingPrice" />
<Suspense>
<template #default>
<Component2 :perServingPrice="data.perServingPrice" />
</template>
<template #fallback>
<div class="skeleton">Loading deals...</div>
</template>
</Suspense>
<Component3 :landingContentKey="landingContentKey" />
<Suspense>
<template #default>
<Component6 :recipes="data.recipes" />
</template>
<template #fallback>
<div class="skeleton">Loading recipes...</div>
</template>
</Suspense>
<Component7 :landingContentKey="landingContentKey" />
<Suspense>
<template #default>
<Component8 :recipes="data.recipes" />
</template>
<template #fallback>
<div class="skeleton">Loading trending dishes...</div>
</template>
</Suspense>
</div>
</template>
๐ฆ Combining with Async Imports
Vue also allows lazy-loading the component itself, not just the data.
<script setup lang="ts">
import { defineAsyncComponent } from 'vue'
const Component6 = defineAsyncComponent(() =>
import('@/components/HomePage/Component6.vue')
)
</script>
Now:
- If the component file is heavy, Vue won’t even load it until needed.
- Suspense covers both the network request and the async component loading.
๐ Before vs After
Without Suspense:
- Whole page waits until all API calls resolve.
- User sees blank โ page suddenly appears.
With Suspense:
- Page renders instantly with placeholders.
- Components hydrate individually as data arrives.
- User perceives speed even if data is slow.
๐ Best Practices
- Wrap only async components. Don’t spam
<Suspense>everywhere. - Always provide a meaningful fallback. Use skeleton loaders, not just “Loadingโฆ”.
- Lift state when appropriate. If multiple components need the same data, fetch once in the parent and pass it down as props.
- Combine Suspense with code-splitting. Async imports keep your initial bundle small.
- Group related components. You can wrap multiple components in a single
<Suspense>if they depend on the same async source.
โ Conclusion
With Vue 3 <Suspense>, you can make sure that your homepage never blocks while waiting for data. Each component becomes non-blocking and self-contained, showing a loader until it’s ready.
This is the same direction React and Angular have taken:
- React โ Suspense + Concurrent Rendering.
- Angular โ Route Resolvers + AsyncPipe.
- Vue โ
<Suspense>+ async setup.
๐ If you want your Vue pages to feel fast and modern, adopt <Suspense> for async components.
Happy Vue Coding!