skip to content

In Angular, why can a component used inside a @defer block still end up in the initial bundle instead of a lazy chunk?

level: middleimportance: must knowfreq 48%

answer

  1. the compiler must drop the import
  2. standalone only
  3. no other value reference in the file
  4. barrels and shared import lines

basics

~20 s

Angular defers a dependency only if it is standalone and nothing else in the same file references it as a value. A reference outside the block, a viewChild query, a non-standalone class or a barrel import keeps it eager.

solid answer

~40 s

The compiler can replace a static `import` with a dynamic `import()` only if it can delete the original import. So a dependency is deferred only when it is **standalone** and is not referenced as a value anywhere else in that file: not in the template outside `@defer`, not in a `viewChild`/`@ViewChild` query, not in class code, and not sharing an import declaration with a symbol that is used eagerly. Type-only references are fine because they vanish from the emitted JavaScript. Importing through a barrel `index.ts` is another trap: the bundler treats the barrel as one module and keeps its exports together, so the component can land in the main bundle anyway. You confirm it in a production build: the component should appear in its own lazy chunk.

code

ts · 18 lines
ts
import { Component, viewChild } from '@angular/core';
import { VideoPlayer } from './video-player/video-player';

@Component({
  selector: 'app-course-page',
  imports: [VideoPlayer],
  template: `
    @defer (on interaction(playBtn)) {
      <app-video-player #player />
    } @placeholder {
      <button #playBtn type="button">Play lesson</button>
    }
  `,
})
export class CoursePage {
  // string locator: VideoPlayer is used only as a type here
  readonly player = viewChild<VideoPlayer>('player');
}

go deeper

for a junior

Know that only standalone components used solely inside the block are split into a lazy chunk.

for a middle

Explain why the compiler must be able to drop the static import, and list the references that prevent it.

for a senior

Verify chunks in production builds, fix viewChild and shared-import references, and remove barrels from deferred paths.

for a principal

Add a build check or review rule for deferred components, since a silent eager fallback erodes bundle budgets over time.

## How deferral works in the compiler For each **`@defer` block**, the Angular compiler collects the components, directives and pipes used in the main content and generates a function that loads them with **dynamic `import()`**. For that to shrink the initial bundle, the file must no longer contain a **static** `import` of the same symbols; otherwise the bundler still pulls the code in eagerly. So the question the compiler asks is: *can I delete this import declaration?* If not, the dependency stays eager, silently. ## Rule 1: the dependency must be standalone Only **standalone** components, directives and pipes can be deferred. A component declared in an **NgModule** is still usable inside `@defer`, but it is loaded eagerly with its module. Since v19, `standalone: true` is the default for new components, so this mostly bites in older code. Transitive dependencies are different: the deferred standalone component may itself import NgModule-based pieces, and those are loaded along with it in the lazy chunk. ## Rule 2: no other value reference in the same file Any reference that survives into the emitted JavaScript keeps the import alive: - the component's selector used **outside** the `@defer` block in the same template; - a **query** on it: `viewChild(VideoPlayer)`, `@ViewChild(VideoPlayer)`; - class code using it as a value: `instanceof VideoPlayer`, `inject(VideoPlayer)`, a static field; - another symbol **in the same import declaration** that is used eagerly. The compiler drops a declaration only if *every* symbol in it is deferrable: ```ts // keeps VideoPlayer eager: PLAYER_DEFAULTS is used at top level import { VideoPlayer, PLAYER_DEFAULTS } from './video-player'; ``` References only in type positions (`player?: VideoPlayer` or `import type`) are fine; they disappear in the emitted code. ## Rule 3: barrels can defeat the chunk A **barrel** file (`index.ts` re-exporting many components) is a single module to the bundler, which keeps its exports together. Angular's own guide names barrels as the common reason a deferred component shows no lazy chunk: it ends up in the main bundle with its neighbours, and the risk is highest when any eager code imports the same barrel. Import deferred components from their own file: ```ts import { VideoPlayer } from './video-player/video-player'; ``` ## A checklist when the chunk is missing 1. Build for production and look for a lazy chunk containing the component. 2. Check that the component is standalone (not declared in an NgModule). 3. Search the file for the class name outside the import line: queries, class code, template use outside the block. 4. Check the import line: does it bring in other symbols used eagerly? Split it. 5. Check for barrels, including a barrel imported eagerly somewhere else in the app. 6. Remember that the other sub-blocks are eager: a component used in `@placeholder` is not deferred, and using the *same* component there makes the main one eager too. ## Common mistakes compared | Code | Deferred? | Why | |---|---|---| | `@defer { <app-video-player /> }`, standalone, imported alone | yes | the import can be removed | | same, plus `viewChild(VideoPlayer)` in the class | no | query keeps a value reference | | same, plus `<app-video-player />` in `@placeholder` | no | used outside the main content | | component declared in an NgModule | no | only standalone dependencies are deferred | | `player = viewChild<VideoPlayer>('player')` with a string locator | yes | the class appears only as a type | The last row is the standard workaround when you need a handle on a deferred component: query by template reference name and use the class only as a type.

  • Why does a type annotation like player?: VideoPlayer not break deferral?
    Type positions are erased when TypeScript emits JavaScript, so they leave no runtime reference. The compiler ignores references in type nodes when deciding whether an import can be dropped. A value use such as `viewChild(VideoPlayer)` or `instanceof VideoPlayer` survives emit and keeps the import eager.
  • Can a deferred standalone component depend on an NgModule?
    Yes. The standalone rule applies to what the `@defer` block uses directly. That component's own transitive dependencies may be NgModule-declared, and they load inside the lazy chunk with it.

saying these in an interview costs you the question

  • Anything written inside @defer is always lazy-loaded.
  • NgModule-declared components are deferred as long as they are inside the block.
  • Using the class only as a TypeScript type forces eager loading.
  • A viewChild query on the deferred class does not affect bundling.
  • Barrel imports never matter because the compiler emits a dynamic import.