In Vue Router 5, what does opening `/billing/invoices` download when parent and child are lazy, and how do you bundle both together?
answer
- one loader, one split point
- entering records load together
- the parent is cached afterwards
- grouping is the bundler's job
- fewer requests versus unused code
basics
~20 sBoth chunks, requested together: the router calls the loader of every record the navigation enters and confirms once all resolve. Later sibling visits load only the new child. Merging them into one chunk is bundler configuration.
solid answer
~40 sIn Vue Router 5 every `() => import()` is its own split point, so a lazy `/billing` layout and a lazy `invoices` child are two chunks. When a navigation enters `/billing/invoices` for the first time, the router calls the loaders of all entering records while collecting their `beforeRouteEnter` guards, so both requests start together, and the navigation confirms once both have resolved. Moving on to `/billing/payments` leaves the billing record matched, so only the payments loader runs, and the layout comes from the router's cache. The router has no option to merge chunks: that is bundler configuration, which the router docs show with Vite's `build.rollupOptions.output.manualChunks` and webpack's `webpackChunkName` comment. Grouping saves requests for pages usually visited together, but every user of one page then downloads the rest of the group.
code
ts · 21 lines// vite.config.ts: one chunk for the billing area, as shown in the Vue Router guide
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
export default defineConfig({
plugins: [vue()],
build: {
rollupOptions: {
output: {
manualChunks: {
'group-billing': [
'./src/billing/BillingLayout.vue',
'./src/billing/Invoices.vue',
'./src/billing/Payments.vue',
'./src/billing/Plans.vue',
],
},
},
},
},
})go deeper
Recall that every () => import() in the route table becomes its own chunk that downloads when that route is first visited.
Explain entered versus updated records, why the parent and child load together on first entry, and why later tab switches only load the new child.
Choose grouping by usage: bundle a feature area together when users move within it, keep rarely paired pages apart, and measure first-entry cost.
Align chunk boundaries with feature ownership and permissions, so a tenant's enabled modules decide what it downloads.
## Every loader is a split point In a Vue Router 5 dashboard, the billing area is a nested route: a `/billing` layout with tabs and a `<RouterView>`, and children for invoices, payments and plans. Each record can have its own loader: ```ts { path: '/billing', component: () => import('./billing/BillingLayout.vue'), children: [ { path: 'invoices', name: 'invoices', component: () => import('./billing/Invoices.vue') }, { path: 'payments', name: 'payments', component: () => import('./billing/Payments.vue') }, { path: 'plans', name: 'plans', component: () => import('./billing/Plans.vue') }, ], } ``` By default the bundler emits one chunk per dynamic `import()`, so this area is four chunks. ## What entering `/billing/invoices` loads The router distinguishes records a navigation **enters**, **updates** (still matched) and **leaves**. Coming from the dashboard home, both `/billing` and `invoices` are entered. The router then: 1. runs `beforeEach`, update guards and the entered records' `beforeEnter`; 2. walks the entered records and **calls every loader** while collecting their `beforeRouteEnter` guards, so the layout and invoices requests are in flight **at the same time**; 3. waits for each, in record order, running each loaded component's `beforeRouteEnter`; 4. runs `beforeResolve` and confirms. If either chunk fails, the whole navigation fails. Afterwards: | Next navigation | Records entered | Chunks requested | |---|---|---| | `/billing/payments` | `payments` | payments only | | back to `/billing/invoices` | `invoices` | none, cached | | `/analytics` then `/billing/plans` | `/billing`, `plans` | plans only; the layout is cached | Once a loader has resolved, the router stores the component in the record, so the layout's chunk is fetched once per page load no matter how often the user comes back. ## Putting them in one chunk The router has **no option** for this: it only calls the functions it was given. Grouping is done by the bundler, and the Vue Router guide shows two ways: - **Vite**: list the modules under a named group in `build.rollupOptions.output.manualChunks`, for example a `group-billing` entry holding the layout and its three children; - **webpack**: give each import the same magic comment, `import(/* webpackChunkName: "group-billing" */ './billing/Invoices.vue')`, and webpack merges them. The loaders in the route table stay exactly as they are; they simply resolve from the same file. Because bundler options change between major versions, check the current option names in your bundler's own documentation. ## When grouping pays off | Choice | Wins | Costs | |---|---|---| | one chunk per page (default) | users download only what they open | more requests on first entry to an area | | one chunk per area | one request for the whole area | users of one tab download every tab | | layout in the main bundle | no request for the frame | every user downloads it, billing or not | In a multi-tenant dashboard, grouping per **feature area** matches how permissions work: a tenant with billing enabled usually opens several billing tabs, and a tenant without it downloads none of them. ## Checking what actually loads Chunk boundaries are easy to get wrong silently, so verify them: 1. read the bundler's build output, which lists every emitted chunk and its size, and confirm the billing modules land where you expect; 2. open the browser's network panel, clear it, and enter `/billing/invoices` from the home page: you should see the requests you planned, and nothing on the next tab switch except the new child; 3. repeat after a static `import` sneaks into a shared file, the usual way a lazy page ends up in the main bundle. ## Named views and lazy parents The same rules apply to named views: with `components: { default: () => import(...), sidebar: () => import(...) }`, each entry is its own loader and both are requested when the record is entered. A parent record also does not need a component at all; a record without one simply adds a path prefix, and its children still load lazily. ## Common mistakes - Expecting the router to fetch the child only after the parent has arrived. - Adding a router option to merge chunks; there is none. - Grouping unrelated pages into one chunk and inflating every first visit. - Statically importing the layout into the routes file "to share it", which moves it into the main bundle.
- Does the router wait for the billing layout before it starts requesting the invoices chunk?No. While collecting the entering records' `beforeRouteEnter` guards the router calls each record's loader, so both requests start together. It then waits for them in record order before confirming, and a failure of either one fails the navigation.
- Why is the layout's chunk not requested again when the user returns to billing from analytics?After a loader resolves, the router replaces the function in the route record with the resolved component. The next navigation that enters `/billing` finds a component, not a loader, so there is nothing to fetch; only children that were never visited still hold loaders.
saying these in an interview costs you the question
- The router loads the parent chunk first and the child only after it arrives
- createRouter has an option that merges lazy routes into one chunk
- Moving between two billing tabs downloads the layout chunk again
- Grouping every page into one chunk is always faster
- Named views load only the default view lazily