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?
answer
- one static import undoes it
- barrel files and shared imports
- component: instead of loadComponent
- initial versus lazy chunk list
- stats.json analysis
basics
~20 sSome 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.
solid answer
~40 s`import()` only creates a separate chunk if nothing on the eager path imports the same module statically. Common culprits: the routes file still has `import { ReportsPage } from './reports/reports-page'` (left over from `component: ReportsPage`, or used for a type), an eager component lists `ReportsPage` in its `imports` to use its selector, or an `index.ts` barrel re-exports it and eager code imports anything from that barrel. `ng build` prints initial and lazy chunk files separately, so ReportsPage's weight shows up in the wrong list; `ng build --stats-json` writes a `stats.json` you can open in esbuild's bundle analyzer to see which import pulled it in. Fix the import (use `import type` for types, deep-import from non-barrel files) and rebuild.
code
ts · 16 linesimport { Routes } from '@angular/router';
// BUG: this static import keeps ReportsPage in the initial bundle
import { ReportsPage } from './reports/reports-page';
// OK: erased at compile time, creates no runtime import
import type { ReportFilter } from './reports/report-filter';
export const routes: Routes = [
{
path: 'reports',
loadComponent: () => import('./reports/reports-page').then((m) => m.ReportsPage),
},
];
export function reportLabel(page: typeof ReportsPage, filter: ReportFilter): string {
return `${page.name}:${filter.kind}`;
}go deeper
Know that a component used with loadComponent must not also be imported normally from eagerly loaded code.
Explain that a split exists only when a file is reachable solely through import(), and list the static-import culprits.
Trace a bundle regression with the build's chunk lists and stats.json, fix the import edge, and add a budget so it cannot recur.
Define module boundary rules for feature areas, including barrel policy and budgets, so lazy boundaries survive many teams editing the code.
## Why a lazy route can end up eager `loadComponent: () => import('./reports/reports-page')` is a **request** to split, not a guarantee. The bundler splits a module into its own chunk only if it is reachable **exclusively** through dynamic `import()` calls. If any module in the initial graph imports the same file with a static `import`, the file is needed at startup, so it is placed in an initial chunk, and the dynamic import simply resolves to the code that is already loaded. The route still works, which is why the mistake goes unnoticed. ## The usual culprits | Culprit | What it looks like | Fix | |---|---|---| | Leftover eager import in the routes file | `import { ReportsPage } from './reports/reports-page';` kept after switching from `component: ReportsPage` | delete it | | Importing the class for a type | a guard or resolver typed against `ReportsPage` | `import type { ReportsPage }`, which is erased at compile time | | Using the component eagerly elsewhere | the dashboard lists `ReportsPage` in its `imports` to render `<app-reports-page>` | extract a small shared widget, or wrap that usage in `@defer` | | Barrel re-export | `reports/index.ts` does `export * from './reports-page'` and eager code imports `ReportsService` from `'./reports'` | import from the specific file, or keep the barrel out of eager code | | Shared constant in the component file | eager code imports `REPORT_TYPES` from `reports-page.ts` | move shared constants to their own file | Barrels deserve special attention. Whether the bundler can drop the unused parts of a barrel depends on side-effect analysis and how the barrel is written, so treating a barrel as safe to import from eager code is a gamble; deep imports make the dependency explicit. ## How to confirm it 1. **Read the build summary.** `ng build` lists **Initial chunk files** and **Lazy chunk files** separately, naming lazy chunks after the lazily imported file where it can. A healthy setup shows a lazy chunk named after the reports page; a broken one shows no such lazy chunk and a larger initial total. 2. **Generate stats.** `ng build --stats-json` writes a `stats.json` file that esbuild's bundle analyzer can open. Look up `reports-page.ts` and follow its **importers**: the first eager module in that chain is the culprit. 3. **Search the eager graph.** A text search for the file's path across `app.routes.ts`, `app.ts` and their imports usually finds the static import in seconds. 4. **Check the network panel.** Loading `/` in the browser should not fetch the reports code; navigating to `/reports` should fetch a new chunk. ## Services can leak across the boundary too Components are not the only culprits. A `providedIn: 'root'` service that only the reports area uses is tree-shaken into the reports chunk, because nothing eager references its class. As soon as an eager component calls `inject(ReportsExportService)`, the class and everything it imports move into the initial bundle. The same happens with a route-level provider listed in the eagerly loaded `app.routes.ts`: the class is referenced from eager code, so it ships eagerly even though it is only instantiated when the route is matched. Checking the importers of a surprising module in `stats.json` finds these edges the same way. ## Keeping the split healthy - Make the lazy file's public surface small: the component and nothing that eager code needs. - Keep shared types, constants and services in separate files that both sides import. - Prefer `import type` for type-only references; TypeScript erases them, so they never create a runtime edge. - Add a bundle budget for the initial chunk in the build configuration so a regression fails the build instead of shipping quietly. - Review route files in code review for static imports of components that also appear in `loadComponent`. Splitting strategy in general, such as how to group vendor code or when to split further, is a bundling question; the Angular-specific point is that `loadComponent` and `loadChildren` only mark the boundary, and one static import on the eager side erases it.
- The dashboard shows a small reports preview using the reports page's component. How do you keep the reports route lazy?Either extract the preview into its own small standalone component that does not import the reports page, or wrap the preview in a `@defer` block in the dashboard template so its dependencies become a deferred chunk. Both remove the static edge from eager code to `reports-page.ts`.
- Why does the app keep working even though the split is broken?The dynamic `import()` still resolves; it just returns a module that is already in the initial bundle. The router loads the component and renders it as usual, so nothing fails at runtime. The only symptom is a larger initial download, which is why build output checks or bundle budgets are needed to catch it.
saying these in an interview costs you the question
- loadComponent always produces a separate chunk regardless of other imports
- import type statements pull the component into the bundle
- Barrel files are always tree-shaken safely
- A broken split makes the lazy route fail at runtime
- Preloading moves the lazy chunk into the initial bundle