In Angular, how do you lazy-load a route with loadComponent versus loadChildren, and what does a default export let you omit?
answer
- one component versus a routes file
- a function returning import()
- standalone components only
- no .then() with export default
basics
~10 sloadComponent takes a function returning import() of one standalone component; loadChildren returns a module whose export is a Routes array. When the file uses export default, the .then(m => m.X) step can be dropped.
solid answer
~40 sBoth properties take a loader function, usually `() => import('./path')`, so the bundler emits the target as a separate chunk that the router downloads only when the route is needed. `loadComponent` is for a single standalone component rendered at that route. `loadChildren` loads a whole child route configuration, typically a `reports.routes.ts` file exporting a `Routes` array, and can still load an NgModule in older code. Normally you select the export with `.then((m) => m.ReportsPage)`, but if the file uses `export default`, the router unwraps the default export itself, so `() => import('./reports-page')` is enough. The loader runs inside the route's injection context, so it may call `inject()`.
code
ts · 14 linesimport { Routes } from '@angular/router';
import { HomePage } from './home/home-page';
export const routes: Routes = [
// eager: part of the initial bundle
{ path: '', component: HomePage },
// lazy single component; reports-page.ts uses `export default class ReportsPage`
{ path: 'reports/summary', loadComponent: () => import('./reports/reports-page') },
// lazy route tree; reports.routes.ts exports a named Routes array
{
path: 'reports',
loadChildren: () => import('./reports/reports.routes').then((m) => m.REPORTS_ROUTES),
},
];go deeper
Recall the two properties, that each takes a function returning import(), and the difference between one component and a routes file.
Explain when each chunk downloads during navigation, the standalone requirement, and how default exports are unwrapped by the router.
Decide what stays eager, how feature areas are grouped into loadChildren boundaries, and how a static import can silently undo a split.
Set team conventions for feature boundaries and export style so lazy chunks stay aligned with how users move through the app.
## Eager versus lazy in the route table A route with `component: ReportsPage` references the class directly, so the build includes it in the same JavaScript bundle as the routes file; for the root routes that is the **initial bundle** every user downloads. Angular's router offers two properties that instead take a **loader function** returning a `Promise` (or an `Observable`) of what to load: | Property | Loads | Typical file | When it downloads during a navigation | |---|---|---|---| | `loadComponent` | one **standalone** component | `reports-page.ts` | after guards and resolvers, just before activation | | `loadChildren` | a child route configuration (`Routes`), or an NgModule in older code | `reports.routes.ts` | during route matching, because the router needs the child routes to match the rest of the URL | The loader usually wraps a dynamic `import()`. Because the path in `import()` is a literal, the build tool can see it and put the target file, and anything only it imports, into a separate **lazy chunk**. ## `loadComponent` ```ts { path: 'reports', loadComponent: () => import('./reports/reports-page').then((m) => m.ReportsPage) } ``` The loaded class must be a standalone component. In development mode the router throws if it gets an NgModule (the message tells you to use `loadChildren` instead) or a non-standalone component. Since v19, components are standalone by default, so a new component needs no extra flag. ## `loadChildren` with a routes file ```ts // app.routes.ts { path: 'reports', loadChildren: () => import('./reports/reports.routes').then((m) => m.REPORTS_ROUTES) } // reports/reports.routes.ts export const REPORTS_ROUTES: Routes = [ { path: '', component: ReportsHome }, { path: ':id', component: ReportDetail }, ]; ``` The whole reports area, its child routes and the components they reference directly, becomes one lazy chunk, loaded the first time a URL under `/reports` is matched. A lazily loaded `Routes` array must use standalone components. Older applications return an NgModule class instead (`then((m) => m.ReportsModule)`); the router compiles it and uses the module's own injector for the children. That form still works but is not what new code uses. ## What a default export changes If the lazy file declares `export default class ReportsPage` or `export default [...] as Routes`, the `.then(...)` step can be dropped: ```ts { path: 'reports', loadComponent: () => import('./reports/reports-page') }, { path: 'admin', loadChildren: () => import('./admin/admin.routes') }, ``` The router detects a module object with a `default` property and unwraps it (the `DefaultExport<T>` type in the route typings allows this). It changes only the syntax: the chunk and the loading behaviour are the same. Some teams prefer named exports for grep-ability and consistent imports; both are valid. ## Details interviewers follow up on - **The loader runs in an injection context**, the route's environment injector, so it can call `inject()`; a loader can, for example, pick between two component files based on an injected feature-flag service. - **A route may have both** `loadComponent` (its own layout component) and `loadChildren` (its children). - **The loaded result is cached on the route**: later navigations reuse it without calling the loader again. If the download fails, nothing is cached and the next navigation tries again. - **The lazy path must not also be imported statically** from eager code, or the build has to include it in the eager bundle and the split is lost. ## What the user sees while a chunk downloads A lazy route does not render a fallback on its own. While the chunk is in flight, the navigation is still pending, so the previous page stays on screen; the new component appears only once the chunk has arrived and been evaluated. If the download fails, for example because the network dropped, the navigation fails with a `NavigationError` and the user stays where they were; nothing is cached, so clicking again retries the load. That is why apps with slow networks often add a global progress indicator for pending navigations, or preload the chunks users are likely to need next. ## Choosing between them 1. One screen with no child routes: `loadComponent`. 2. A feature area with several routes, shared route-level providers or its own guards: `loadChildren` pointing at a routes file. 3. The landing page and anything on the first screen: usually eager, because making it lazy only adds a round trip before first render.
- What happens if loadComponent points at a component that is not standalone?In development mode the router throws an invalid-route-configuration error when the chunk arrives: it says the component must be standalone, or, if it received an NgModule, that `loadChildren` should be used instead. Since v19 components are standalone unless they set `standalone: false`, so this mostly bites during NgModule migrations.
- Why does loadChildren download during route matching while loadComponent waits until activation?The router cannot match the rest of the URL, for example `/reports/42`, without knowing the child routes, so it must fetch the `loadChildren` chunk while recognising the URL. A `loadComponent` chunk only supplies what to render, so the router loads it after guards and resolvers, just before it activates the route.
saying these in an interview costs you the question
- loadComponent can point at an NgModule
- loadChildren only accepts NgModules
- A default export makes the chunk smaller
- A string path such as './reports#ReportsModule' is still supported
- The router re-downloads the chunk on every navigation