Tutorials Logic, IN info@tutorialslogic.com

Vue Async Components Suspense Lazy Loading

Async Loading Boundary

Async components defer downloading and evaluating a component until it is needed. Use defineAsyncComponent for a heavy, optional interface region such as an editor or report, while route-level dynamic imports split entire screens. The learner should be able to choose a boundary, show loading and failure states, and avoid splitting tiny components that only add requests.

Async Components

Async components let you load components lazily - only when they're needed. This reduces the initial bundle size and improves page load performance. Vue's defineAsyncComponent() wraps a dynamic import and handles loading/error states.

defineAsyncComponent and Suspense

defineAsyncComponent and Suspense
<template>
  <div>
    <!-- Async component - loaded only when rendered -->
    <HeavyChart v-if="showChart" />
    <button @click="showChart = !showChart">Toggle Chart</button>

    <!-- With loading and error states -->
    <AdminPanel v-if="isAdmin" />
  </div>
</template>

<script setup>
import { ref, defineAsyncComponent } from 'vue'

// Simple async component - just a dynamic import
const HeavyChart = defineAsyncComponent(() =>
  import('./HeavyChart.vue')
)

// With options - loading state, error state, timeout
const AdminPanel = defineAsyncComponent({
  // The loader function
  loader: () => import('./AdminPanel.vue'),

  // Component to show while loading
  loadingComponent: {
    template: '<div class="loading-spinner">Loading...</div>'
  },

  // Delay before showing loading component (ms)
  delay: 200,

  // Component to show if loading fails
  errorComponent: {
    template: '<div class="error">Failed to load component</div>'
  },

  // Timeout - show error if loading takes too long
  timeout: 5000,

  // Called when loading fails
  onError(error, retry, fail, attempts) {
    if (attempts <= 3) {
      retry()  // retry up to 3 times
    } else {
      fail()
    }
  }
})

const showChart = ref(false)
const isAdmin = ref(true)
</script>

Async Components - HTML Example

Async Components - HTML Example
<!-- Suspense - handle async setup() in child components -->
<template>
  <div>
    <!-- Suspense wraps async components -->
    <Suspense>
      <!-- Default slot: shown when ready -->
      <template #default>
        <AsyncUserProfile :userId="userId" />
      </template>

      <!-- Fallback slot: shown while loading -->
      <template #fallback>
        <div class="skeleton">
          <div class="skeleton-avatar"></div>
          <div class="skeleton-text"></div>
          <div class="skeleton-text short"></div>
        </div>
      </template>
    </Suspense>

    <!-- Combine with Transition for smooth loading -->
    <Suspense @pending="onPending" @resolve="onResolve" @fallback="onFallback">
      <template #default>
        <Transition name="fade" mode="out-in">
          <AsyncDashboard :key="currentPage" />
        </Transition>
      </template>
      <template #fallback>
        <LoadingSpinner />
      </template>
    </Suspense>
  </div>
</template>

<script setup>
import { ref, defineAsyncComponent } from 'vue'

const AsyncUserProfile = defineAsyncComponent(() => import('./UserProfile.vue'))
const AsyncDashboard   = defineAsyncComponent(() => import('./Dashboard.vue'))

const userId = ref(1)
const currentPage = ref('home')

function onPending()  { console.log('Loading started') }
function onResolve()  { console.log('Loading complete') }
function onFallback() { console.log('Showing fallback') }
</script>

<!-- AsyncUserProfile.vue - uses async setup() -->
<!-- <script setup> -->
<!-- const props = defineProps({ userId: Number }) -->
<!-- // await in setup() - Suspense waits for this -->
<!-- const user = await fetch(`/api/users/${props.userId}`).then(r => r.json()) -->
<!-- </script> -->

Loading and Recovery

A loading delay prevents a spinner from flashing on fast connections, while a timeout and error component give slow or failed loads a recoverable state. Retrying should create a fresh import attempt only when the failure can be transient. Keep essential navigation and the first useful screen in the initial path.

Suspense can coordinate async setup dependencies in a subtree, but it does not replace route and request error handling. Verify chunks through the production build and browser network panel; development timing is not representative of cached, compressed deployment assets.

defineAsyncComponent Loading and Failure Controls

defineAsyncComponent accepts a loader that returns an import promise. Vue delays loading until the component is rendered, caches the resolved definition, and forwards props and slots to it. The options form adds loading, error, delay, timeout, and retry behavior for slow or unreliable chunks.

Keep loading and error components small because they are part of the fallback path. A timeout changes the displayed state but does not guarantee the underlying network request is cancelled. Use onError to retry only failures likely to be transient and cap the number of attempts.

  • Use delay to avoid flashing a spinner for fast chunk loads.
  • Use timeout to replace an endless pending state with an actionable error UI.
  • Set suspensible deliberately when the component is nested inside Suspense.
  • Log the chunk URL and release identifier without exposing private route or user data.

Configure an async component boundary

Configure an async component boundary
import { defineAsyncComponent } from 'vue'
import LoadingPanel from './LoadingPanel.vue'
import LoadError from './LoadError.vue'

export const ReportsPanel = defineAsyncComponent({
  loader: () => import('./ReportsPanel.vue'),
  loadingComponent: LoadingPanel,
  errorComponent: LoadError,
  delay: 150,
  timeout: 10_000,
  onError(error, retry, fail, attempts) {
    const transient = /fetch|network|loading chunk/i.test(error.message)
    if (transient && attempts <= 2) retry()
    else fail()
  },
})

The boundary provides stable loading, timeout, error, and bounded retry behavior around the dynamic import.

Router Lazy Loading and Suspense Boundaries

For route records, assign the dynamic import function directly to component so the router loads that chunk only when navigation needs it. Do not wrap route components in defineAsyncComponent unless its loading behavior is specifically required. Group chunks only when the routes are commonly used together; one oversized shared chunk defeats lazy loading.

Suspense coordinates async setup and async components below one boundary. Provide meaningful fallback UI, handle rejected work, and remember that Suspense does not replace route-level error handling or a retry decision. Prefetch critical chunks only when the bandwidth cost is justified by likely navigation.

Before you move on

Vue Async Components Suspense Lazy Loading Mastery Check

3 checks
  • Async components let you load components lazily - only when they're needed.
  • This reduces the initial bundle size and improves page load performance.
  • Vue's defineAsyncComponent() wraps a dynamic import and handles loading/error states.
Browse Free Tutorials

Explore 500+ free tutorials across 20+ languages and frameworks.