Modern Angular: Signals, Standalone Components and SSR · Routing, State, Performance and SSR

Rendering Performance: track, @defer and Zoneless Change Detection

Keep rendering fast and correct with proper @for tracking, lazy @defer blocks, and signal-driven zoneless change detection.

Create a free account to save this page, take notes and track your progress. Sign in

What you'll build

Three small experiments: a list where tracking by $index moves a typed note onto the wrong item after reordering (and tracking by id doesn't), a @defer block whose placeholder, loading and complete states are stepped through in a test, and a zoneless component showing that a plain field change isn't rendered until a signal change schedules change detection.

The idea

  • track identifies items; with $index, DOM nodes (and their state, like input values) stay at positions instead of following items.
  • @defer (on viewport) (also on idle, on interaction, on timer, when cond) loads a block's components lazily in a separate chunk, with @placeholder, @loading and @error blocks.
  • Angular apps are zoneless by default in current versions: change detection runs when signals used in templates change, on template events, and on markForCheck, not after every timer or promise. Signals make updates explicit and cheap.

The code

track, @defer states and zoneless updates
import { Component, signal } from "@angular/core";
import { DeferBlockBehavior, DeferBlockState } from "@angular/core/testing";
import { TestBed } from "@angular/core/testing";
import { BrowserTestingModule, platformBrowserTesting } from "@angular/platform-browser/testing";

@Component({
  selector: "app-list",
  template: `
    @for (item of items(); track trackBy === "id" ? item.id : $index) {
      <label>{{ item.name }} <input /></label>
    }
  `,
})
class NotesList {
  trackBy: "id" | "index" = "id";
  items = signal([{ id: 1, name: "Mug" }, { id: 2, name: "Teapot" }]);
}

@Component({
  selector: "app-product",
  template: `
    <h1>Teapot</h1>
    @defer (on viewport) { <section>Reviews: 4.8 stars</section> }
    @placeholder { <p>Reviews placeholder</p> }
    @loading { <p>Loading reviews...</p> }
  `,
})
class ProductPage {}

@Component({ selector: "app-clock", template: `<p>plain {{ plain }}, signal {{ ticks() }}</p>` })
class Clock {
  plain = 0;
  ticks = signal(0);
}

TestBed.initTestEnvironment(BrowserTestingModule, platformBrowserTesting());

for (const trackBy of ["index", "id"] as const) {
  const fixture = TestBed.createComponent(NotesList);
  fixture.componentInstance.trackBy = trackBy;
  await fixture.whenStable();
  fixture.nativeElement.querySelector("input").value = "gift for mum";        // typed next to "Mug"
  fixture.componentInstance.items.update((list) => [...list].reverse());
  await fixture.whenStable();
  const rows = [...fixture.nativeElement.querySelectorAll("label")] as HTMLElement[];
  console.log(`track by ${trackBy}: ` + rows.map((r) => `${r.textContent!.trim()}="${r.querySelector("input")!.value}"`).join(", "));
}

TestBed.resetTestingModule();
TestBed.configureTestingModule({ deferBlockBehavior: DeferBlockBehavior.Manual });
const page = TestBed.createComponent(ProductPage);
await page.whenStable();
const [reviews] = await page.getDeferBlocks();
for (const state of [DeferBlockState.Placeholder, DeferBlockState.Loading, DeferBlockState.Complete]) {
  await reviews.render(state);
  const parts = [...page.nativeElement.querySelectorAll("h1, p, section")].map((n: Element) => n.textContent!.trim());
  console.log(`defer ${DeferBlockState[state]}: ${parts.join(" | ")}`);
}

const clock = TestBed.createComponent(Clock);
await clock.whenStable();
clock.componentInstance.plain = 5;                                           // not a signal: nothing scheduled
await new Promise((resolve) => setTimeout(resolve, 20));
console.log(`zoneless, plain field changed: ${clock.nativeElement.textContent}`);
clock.componentInstance.ticks.set(1);                                       // signal: schedules rendering
await clock.whenStable();
console.log(`zoneless, signal changed: ${clock.nativeElement.textContent}`);

Output

track by index: Teapot="gift for mum", Mug=""
track by id: Teapot="", Mug="gift for mum"
defer Placeholder: Teapot | Reviews placeholder
defer Loading: Teapot | Loading reviews...
defer Complete: Teapot | Reviews: 4.8 stars
zoneless, plain field changed: plain 0, signal 0
zoneless, signal changed: plain 5, signal 1

Run this on your computer — see “Get Started” for setup.

With $index, the note typed next to "Mug" stays in the first row, which now shows "Teapot". The defer block shows its placeholder until its trigger fires. The plain field change only appears once a signal change triggers rendering.

Common mistakes

  • track $index (or tracking whole objects that get recreated) for lists that reorder or reload.
  • Deferring above-the-fold content, delaying what users see first; defer below-the-fold or interaction-only UI.
  • Updating plain fields from timers or callbacks in zoneless apps and wondering why the view doesn't change; use signals.
  • Heavy computations in template expressions; use computed.

Key points

  • Track by stable IDs so DOM state follows items.
  • @defer splits rarely needed UI into lazy chunks with placeholder and loading states.
  • Zoneless change detection runs when signals change: keep view state in signals.

Further reading: Deferred loading with @defer · Zoneless · @for track

Practice questions

What goes wrong with track $index on a reorderable list?

Answer

DOM nodes stay attached to positions, so per-row state such as typed input values ends up next to a different item after reordering.

What does @defer (on viewport) do?

Answer

It lazily loads the block's dependencies in a separate chunk and renders it when its placeholder scrolls into view, showing the @placeholder until then.

What triggers change detection in a zoneless Angular app?

Answer

Changes to signals read in templates, template event handlers, markForCheck, and a few framework APIs; not arbitrary timers or promises.

Open in the app to mark lessons complete, take notes and track your progress.

Was this page helpful?