In Vue Router 5, how do you lazy-load a dashboard route's component, and what breaks if you write `component: import('./Billing.vue')` without the arrow function?
answer
- a function, not a promise
- called on first entry, then cached
- the router waits before confirming
- the default export is used
- import() runs when the file loads
basics
~20 sGive the route a loader, component: () => import('./Billing.vue'); the router calls it on first entry, waits for it and caches the result. Without the arrow the import runs at startup, and development warns that production will break.
solid answer
~50 sIn Vue Router 5, `component` and `components` accept a function that returns a promise of a component, usually `() => import('./Billing.vue')`. The bundler turns each dynamic import into a separate chunk, and the router calls the loader only when a navigation first enters that route, waits for it before confirming the navigation, uses the module's default export, and replaces the loader with the resolved component so later visits are instant. Writing `component: import('./Billing.vue')` passes a promise instead of a function: the import starts as soon as the routes file is evaluated, so the chunk downloads at startup, and development builds warn that the record holds a promise and that this will break in production, where the router treats the promise as if it were a component. If a loader fails, the navigation fails, `router.onError()` handlers run, and the next attempt calls the loader again.
code
ts · 15 linesimport { createRouter, createWebHistory } from 'vue-router'
import HomePage from './pages/HomePage.vue'
export const router = createRouter({
history: createWebHistory(),
routes: [
{ path: '/', name: 'home', component: HomePage },
{ path: '/billing', name: 'billing', component: () => import('./pages/Billing.vue') },
{ path: '/analytics', name: 'analytics', component: () => import('./pages/Analytics.vue') },
],
})
router.onError((error, to) => {
console.error('navigation to', to.fullPath, 'failed', error)
})go deeper
Recall the form component: () => import('./Page.vue') and that the page's code downloads the first time the route is visited.
Explain when the router calls the loader, that it waits before confirming, uses the default export and caches the result, and why dropping the arrow loads eagerly.
Show you guard the failure path: onError reporting, retries on the next navigation, and review rules that catch promise-valued components before production.
Set the convention that every non-entry page is a loader, and decide which few pages stay in the main bundle for first paint.
## Route-level lazy loading A multi-tenant dashboard ships a dozen feature pages, billing, analytics, reports, audit log, and most users open two or three of them. Bundling all of them into the first download makes every user pay for every page. Vue Router 5 supports **lazy route components** directly: a route's `component` (or each entry of `components`) can be a **function that returns a promise of a component**. ```ts const routes = [ { path: '/', name: 'home', component: HomePage }, { path: '/billing', name: 'billing', component: () => import('./pages/Billing.vue') }, { path: '/analytics', name: 'analytics', component: () => import('./pages/Analytics.vue') }, ] ``` The dynamic `import()` is what the bundler sees: each one becomes a **split point**, and the imported module lands in its own chunk. The router's part is deciding **when** to call the function. ## What the router does with the loader 1. At startup nothing is called; the route table only holds the functions. 2. When a navigation enters `/billing` for the first time, after the global `beforeEach` guards and the route's `beforeEnter`, the router calls the loader. 3. The navigation stays **pending** until the promise resolves; the user keeps seeing the current page. 4. If the promise resolves to an ES module, the router takes its **`default` export**. 5. It **replaces the loader** in the route record with the resolved component, so later visits reuse it without another request. 6. It runs the loaded component's `beforeRouteEnter` guard, if any, and continues the navigation. Because step 2 comes after the global guards, a signed-out user redirected by `beforeEach` never downloads the billing chunk. ## Why the arrow function matters | Written as | When the chunk downloads | Result | |---|---|---| | `component: () => import('./Billing.vue')` | on first entry to the route | lazy, as intended | | `component: import('./Billing.vue')` | as soon as the routes file runs | eager, and broken in production | | `import Billing from './Billing.vue'` + `component: Billing` | with the main bundle | not split at all | Without the arrow, `import('./Billing.vue')` is **executed** while the route array is built, so the request starts at startup whether or not anyone opens billing. The record then holds a promise, not a function. Development builds detect this, warn that the record contains a promise instead of a function returning one and that this will break in production, and wrap it so it still works locally. Production builds do not wrap it; the router treats the promise object as a component, and the page fails to render. Code that "works on my machine" and breaks after deploy is exactly what the warning is about. ## When loading fails A loader can reject, for example when the network drops. The router then: - cancels the navigation, so the user stays on the current page; - calls every `router.onError()` handler, or logs the error to the console when none is registered; - makes the `router.push()` promise reject. The loader was not replaced, because nothing was resolved, so the next navigation to billing calls it again. ## Choosing what stays eager Lazy loading is the default for pages, not for everything: - the component for the **first screen** most users land on, often the dashboard home, is usually imported statically so the first paint needs no extra request; - the **app shell**, header, navigation and the layout every page shares, belongs in the main bundle for the same reason; - **rarely visited or permission-gated pages**, such as billing for tenants on a paid plan or the audit log for administrators, are the best candidates for loaders, because most users never pay for them. Where exactly the split points go, and how large the initial bundle may be, is a performance-budget decision rather than a router one. ## A trap with functional components The router has to tell a lazy loader from a **functional component**, since both are plain functions. It treats a function as a component only if it has a `displayName` (or `props`), so a functional route component needs `displayName` set; otherwise the router calls it as if it were a loader. ## Common mistakes - Dropping the arrow and shipping a promise to production. - Statically importing a page at the top of the routes file "just for types", which pulls it into the main bundle. - Expecting the loader to run on every visit; it runs once per record until it succeeds. - Forgetting to register `router.onError()`, so chunk failures only reach the console.
- Why does a redirect in `beforeEach` stop the billing chunk from downloading?The router calls route loaders only after the global `beforeEach` guards and the route's `beforeEnter` have passed. A guard that redirects or cancels ends the navigation before that step, so the loader for `/billing` is never called and its chunk is never requested.
- Can a loader return something other than `import()`?Yes. The router only needs a function returning a promise of a component, so `() => Promise.resolve(Component)` or a function that awaits a feature flag before importing both work. If the promise resolves to a module, the router uses its `default` export; if it resolves to nothing, the navigation fails with an error that the component could not be resolved.
saying these in an interview costs you the question
- component: import('./Billing.vue') is the same as the arrow-function form
- The router calls every route loader at startup to warm the cache
- A lazy route component is fetched again on every visit
- Lazy route components need defineAsyncComponent to work
- A failed route chunk renders an empty page and the navigation completes