skip to content

On-Demand Route Chunks

Splitting routes with loadComponent and loadChildren, then preloading them with PreloadAllModules or a custom PreloadingStrategy. Interviewers probe what ends up in the initial bundle and why.

part ofAngularoverview, primer and where to startread it →
on this pageshow

explore

questions

5

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
open as a page

In Angular, what changes when you add withPreloading(PreloadAllModules) to provideRouter, and when does the router start preloading?

level: middleimportance: should knowfreq 58%

basics

~10 s

By default the router uses NoPreloading, so lazy chunks download only on navigation. withPreloading(PreloadAllModules) makes the router, after each completed navigation, download every lazy loadChildren and loadComponent chunk in the background.

open as a page

In Angular, what does a route's providers array create, which code can inject from it, and how long do those instances live?

level: middleimportance: should knowfreq 44%

basics

~20 s

A route's providers array makes the router create an EnvironmentInjector for that route and its children; their components, guards, resolvers and loaders can inject from it. It lives on after you leave unless withAutoCleanupInjectors is enabled.

open as a page

An Angular reports route uses loadComponent, yet the build still puts ReportsPage in the initial bundle; what usually causes that, and how do you confirm it?

level: seniorimportance: should knowfreq 40%

basics

~20 s

Some eagerly loaded file still imports ReportsPage statically, directly or through a barrel, so the bundler must keep it in the initial chunk. Confirm with the build's initial and lazy chunk lists and a stats.json analysis.

open as a page

How would you write an Angular PreloadingStrategy that preloads the reports section only for signed-in admins, and what must it not be relied on for?

level: seniorimportance: should knowfreq 38%

basics

~20 s

Implement PreloadingStrategy in a root-provided class whose preload() calls load() only when the route is flagged for admins and the injected auth state says the user is one, returning of(null) otherwise. It is a performance hint, never access control.

open as a page