skip to content

In Angular, how do you lazy-load a route with loadComponent versus loadChildren, and what does a default export let you omit?

level: juniorimportance: must knowfreq 70%

answer

  1. one component versus a routes file
  2. a function returning import()
  3. standalone components only
  4. no .then() with export default

basics

~10 s

loadComponent 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 s

Both 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 lines
ts
import { 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

for a junior

Recall the two properties, that each takes a function returning import(), and the difference between one component and a routes file.

for a middle

Explain when each chunk downloads during navigation, the standalone requirement, and how default exports are unwrapped by the router.

for a senior

Decide what stays eager, how feature areas are grouped into loadChildren boundaries, and how a static import can silently undo a split.

for a principal

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