skip to content

An Angular page nests @defer blocks three levels deep, and the network panel shows their chunks loading one after another; why, and how do you fix it?

level: seniorimportance: should knowfreq 24%

answer

  1. inner blocks do not exist yet
  2. discovered only after the outer renders
  3. same trigger means back-to-back fetches
  4. flatten or give distinct triggers

basics

~20 s

An inner Angular @defer block only exists once its outer block renders, so its trigger and fetch start after the outer chunk arrives. Same-timed nested triggers therefore form a waterfall; flatten them into one block or give inner blocks later triggers.

solid answer

~50 s

A nested `@defer` lives in its parent's main content, which is not created until the parent's chunk loads and renders. Only then are the inner block's triggers registered, and only then can its own chunk be requested. If all three blocks use the same immediate-style trigger (default idle, `on immediate`, `on viewport` of something already visible), each level waits a full round trip for the level above: a cascade. Angular's guidance is that nested blocks should have different triggers. If the inner content is always needed as soon as the outer shows, remove the inner `@defer` so its dependencies ride in the outer chunk. If it is genuinely optional, give it a later, user-driven trigger such as `on interaction` or `on viewport` of a lower element, so the second fetch is not on the page-load path.

code

ts · 21 lines
ts
import { Component } from '@angular/core';
import { CourseBody } from './course-body';
import { Transcript } from './transcript';
import { QuizPanel } from './quiz-panel';

@Component({
  selector: 'app-course-page',
  imports: [CourseBody, Transcript, QuizPanel],
  template: `
    @defer {
      <app-course-body />
      <app-transcript />
      @defer (on viewport; prefetch on idle) {
        <app-quiz-panel />
      } @placeholder {
        <div class="quiz-skeleton"></div>
      }
    }
  `,
})
export class CoursePage {}

go deeper

for a junior

Know that an inner @defer block is only created after its outer block has loaded and rendered.

for a middle

Explain why same-timed nested triggers produce sequential chunk requests while siblings can load in parallel.

for a senior

Read the network waterfall, flatten always-needed levels, and give optional inner blocks later, user-driven triggers.

for a principal

Set a defer-boundary guideline per page section so component authors do not stack wrappers that each add a round trip.

## Why nesting creates a waterfall An Angular **`@defer` block** is a template instruction. Its triggers are set up when the view that *contains* it is created. For a nested block, that containing view is the **main content of the outer `@defer`**, which does not exist until the outer block has fetched its dependencies and rendered. ```html @defer { <!-- level 1: default on idle --> <app-course-body /> @defer { <!-- level 2 --> <app-transcript /> @defer { <!-- level 3 --> <app-quiz-panel /> } } } ``` The sequence is strictly serial: 1. Idle time: fetch level 1's chunk, render it. 2. Level 2 now exists; on the next idle period, fetch its chunk, render it. 3. Level 3 now exists; fetch its chunk, render it. Each step includes a network round trip that could not start earlier, because the browser had no way to know about the inner chunk. This is the **cascading load** the Angular documentation warns about: nested blocks should have **different triggers** so they do not load in immediate succession. ## Diagnosing it - The **network waterfall** shows chunk requests starting only after the previous one finished, with nothing in parallel. - In the template, the same trigger (or the implicit `on idle`) at every level is the giveaway. - Removing a level and rebuilding should remove one step from the waterfall; if it does not, the chunk was not really separate (check the deferral rules first). ## Fixes | Situation | Fix | |---|---| | Inner content always needed with the outer content | remove the inner `@defer`; its dependencies join the outer chunk | | Inner content optional or below the fold | give it a later, user-driven trigger: `on interaction`, `on viewport` of a lower element, `on hover` | | Inner content likely needed soon | keep the inner block but prefetch it on a trigger that fires once the outer has rendered | | Deep nesting for organisation only | flatten into sibling blocks at one level so their triggers register together | Sibling `@defer` blocks at the same level register their triggers when their shared parent view is created, so they can fetch **in parallel**. Nesting is what serialises them. ## Shared dependencies If two blocks (nested or siblings) use the same component, Angular and the bundler share that code rather than downloading it twice; the 22.2 compiler also deduplicates deferred imports shared by several blocks. Sharing avoids duplicate bytes, but it does not break a cascade: the inner block's request still cannot start before it exists. ## A worked fix For the course page above, the transcript is always shown with the body, and the quiz is at the bottom and optional: ```html @defer { <app-course-body /> <app-transcript /> @defer (on viewport; prefetch on idle) { <app-quiz-panel /> } @placeholder { <div class="quiz-skeleton"></div> } } ``` - Level 2 is gone: `app-transcript` now loads with the body in one chunk. - The quiz keeps its own block, but with a **viewport** trigger it only renders when reached; `prefetch on idle` fetches its chunk in the first idle period after the outer block renders, so scrolling down finds it ready. - Two round trips remain on the worst path instead of three, and the second is off the critical path. ## Rules of thumb 1. Defer at the **coarsest useful boundary**: one block per independently useful section. 2. Nest only when the inner content is optional relative to the outer, and then give it a **different, later** trigger. 3. Re-check the waterfall after refactors; a new wrapper component with its own `@defer` can add a level without anyone noticing.

  • Can the inner block's prefetch trigger start its download before the outer block renders?
    No. Prefetch triggers are part of the inner block, and the inner block does not exist until the outer content renders. To have the inner code ready earlier, move its dependencies into the outer block, or make the inner block a sibling at a level that already exists.
  • Why do sibling @defer blocks not cascade the same way?
    Siblings live in the same parent view, so all their triggers register when that view is created. If they use the same trigger, their fetches start together and overlap. The serial dependency comes only from nesting, where each inner block is created by its parent's rendered content.

saying these in an interview costs you the question

  • Nested @defer blocks fetch their chunks in parallel at page load.
  • Prefetch on an inner block runs before the outer block renders.
  • Giving every level the same trigger keeps them in sync.
  • Deduplicating shared imports removes the cascade.
  • More nested @defer levels always mean a smaller initial bundle and a faster page.