Implement Lazy-Loaded Routes in Vue 3 with Vue Router 4 and the Composition API
Learn how to split your Vue 3 app's bundle by lazy‑loading routes with Vue Router 4, dynamic imports, and defineAsyncComponent for loading and error states.
25 May 2026, 06:32 UTC

Problem and Takeaway
When a Vue 3 application grows, the initial JavaScript bundle can become large because every view is loaded up front, even if the user never navigates to some pages. Lazy‑loading routes splits the code so each view is fetched only when it is first requested, reducing the initial download size and improving perceived performance.
By the end of this guide you will have a Vue Router 4 instance that loads components on demand using dynamic import() and defineAsyncComponent, with basic loading and error handling in place.
Prerequisites
- Vue 3 (>=3.0) project set up with a build tool that supports ES modules dynamic imports (Vite, Webpack, etc.)
- Vue Router 4 installed (
npm i vue-router@4) - Basic familiarity with the Composition API (
setup(),ref, etc.)
Procedure
-
Create a router file (
src/router/index.js) and import the router creation functions:import { createRouter, createWebHistory } from 'vue-router' import { defineAsyncComponent } from 'vue' const routes = [ { path: '/', name: 'Home', component: () => import('@/views/HomeView.vue') }, { path: '/about', name: 'About', // Wrap the dynamic import for loading/error handling component: defineAsyncComponent({ loader: () => import('@/views/AboutView.vue'), loading: () => import('@/components/LoadingSpinner.vue'), error: () => import('@/components/LoadError.vue'), delay: 200, timeout: 10000 }) }, { path: '/dashboard', name: 'Dashboard', component: defineAsyncComponent(() => import('@/views/DashboardView.vue')) } ] const router = createRouter({ history: createWebHistory(import.meta.env.BASE_URL), routes }) export default router -
Install the router in your main application file (
src/main.js):import { createApp } from 'vue' import App from './App.vue' import router from './router' createApp(App).use(router).mount('#app') -
Use
<router-view>inApp.vueto render the active route:<template> <nav> <router-link to="/">Home</router-link> | <router-link to="/about">About</router-link> | <router-link to="/dashboard">Dashboard</router-link> </nav> <router-view /> </template> -
Optional: add a global loading indicator that shows while the router is resolving async components. In
App.vueyou can watchrouter.isReady:<template> <div> <LoadingSpinner v-if="!isRouterReady" /> <router-view v-else /> </div> </template> <script setup> import { ref } from 'vue' import { useRouter } from 'vue-router' import LoadingSpinner from '@/components/LoadingSpinner.vue' const router = useRouter() const isRouterReady = ref(false) router.isReady().then(() => { isRouterReady.value = true }) </script> -
Navigation guards (
beforeEach,beforeResolve) work unchanged with lazy routes. If you need to guard a lazy route, place the guard as usual:router.beforeEach((to, from, next) => { if (to.meta.requiresAuth && !isLoggedIn) { next({ name: 'Login' }) } else { next() } })
Expected Checks
- Start the development server (
npm run dev). Open DevTools → Network, enable “Disable cache”, and navigate to /about for the first time. You should see a separate chunk request (e.g.,about.[hash].js) being loaded. - Navigate back to /home and then to /about again; the chunk should now be served from the cache (status 200 from (memory cache) or (disk cache)).
- After building for production (
npm run build), inspect thedistfolder. You should find multiple JavaScript files corresponding to each lazy route alongside the main app bundle. - To test error handling, temporarily rename the lazy component file (e.g.,
AboutView.vue→AboutView.bak), reload the app, and navigate to /about. The error component defined indefineAsyncComponentshould appear instead of a blank screen.
Recovery Options
- Provide a fallback UI via the
erroroption ofdefineAsyncComponentas shown above. - Catch import failures globally with
router.onError:
router.onError((err) => {
console.error('Route loading error:', err)
// Optionally redirect to an error page
router.replace({ name: 'ServerError' })
})
Limitations and Practical Tips
- Lazy‑loading only works if your build tool preserves the dynamic
import()syntax. Verify that your Vite/Webpack configuration does not pre‑bundle those imports. - Avoid importing large side‑heavy libraries inside the lazy component itself; otherwise the chunk may not be significantly smaller than the main bundle.
- When using server‑side rendering (Nuxt 3 or manual
vue-server-renderer), async components require additional hydration configuration to avoid mismatches. For pure client‑side SPAs the steps above are sufficient. - If you need to pass props to a lazy component, define them inside the component file; the router does not support inline props with
defineAsyncComponentdirectly.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.