Angular @defer Blocks: The Easiest Performance Win You Are Not Using
A plain-English explanation of Angular @defer blocks — when to use on viewport, on idle, on interaction, and on when, plus the placeholder, loading, and error blocks that make the experience feel right.
If you've built an Angular app in the last couple of years and haven't used @defer yet, you're shipping more JavaScript than you need to. @defer is one of those rare features that is both powerful and easy to add, and most teams leave the easy performance wins on the table.
Here's what @defer does, how to use it without breaking the page, and how it ties into the rest of the Angular performance checklist.
The short version
@defer tells Angular to wait before loading part of a template. Not "lazy load this whole route" — lazy within the same screen. The block is parsed normally; its dependencies and children just aren't downloaded until a condition you choose is met.
``html @defer () { <app-reviews /> } @placeholder { <p>Loading reviews…</p> } @loading (after 100ms; minimum 300ms) { <p>Loading reviews…</p> } ``
That app-reviews component, its template, its styles, and any components it pulls in stay out of the initial bundle. The user gets a smaller first download, and you don't have to think about code-splitting by hand.
Three reasons to use it
1. The component below the fold doesn't belong in the critical path. A comment section, a chat widget, an "also bought" carousel — anything the user can't see on first paint is a candidate.
2. Heavy components shouldn't block the first paint. A chart library, a markdown editor, a map component — anything pulling in tens of kilobytes of JavaScript is a candidate. Same if it only matters after the user clicks something.
3. Third-party widgets are easier to reason about. Slot them into a @defer (on viewport) block and they only load when scrolled into view. No more "the chatbot slowed down our LCP" debates.
For the bigger performance context, see Angular Performance: The Few Things That Actually Matter.
The triggers worth knowing
@defer has a handful of triggers. The first three cover 90% of real cases.
@defer (on viewport) — load when the block scrolls into view. Use it for anything below the fold: testimonials, related products, comment threads.
``html @defer (on viewport; prefetch on idle) { <app-related-posts /> } @placeholder { <div style="min-height: 200px"></div> } ``
The prefetch on idle part is the move: Angular downloads the chunk during the browser's idle time so it's ready by the time the user scrolls. Best of both worlds.
@defer (on idle) — load when the browser is otherwise free. Good for non-critical UI: a settings panel, an "export to CSV" button, secondary navigation.
@defer (on interaction) — load only after the user hovers, clicks, or focuses an element. Use this for anything that's behind a click anyway: an "open filters" drawer, a modal, a heavy form.
```html <button type="button" #trigger>Open filters</button>
@defer (on interaction(trigger)) { <app-filters /> } @placeholder { <span></span> } ```
@defer (when condition) — load when a signal or expression becomes truthy. Useful for "the user signed in," "the data is ready," "the modal is open."
The two you probably shouldn't reach for often: @defer (on hover) and @defer (on timer(...)). Hover triggers fire on touch devices inconsistently, and timers tie loading to clock time instead of user behavior.
The blocks people forget
A @defer block needs the matching placeholder and loading blocks, even if they're empty. Otherwise Angular warns at build time and the experience is jumpy at runtime.
``html @defer (on viewport) { <app-heavy-stuff /> } @placeholder { <!-- empty, takes up the right amount of space --> <div style="min-height: 240px"></div> } @loading (after 100ms; minimum 300ms) { <p>Loading…</p> } @error { <p>This section failed to load. Refresh to try again.</p> } ``
Three quick rules for the blocks:
placeholdermust reserve the same space as the deferred content. If you don't, the page jumps when the content arrives, which kills your CLS score — see Core Web Vitals and SEO. Amin-heighton a wrapper is usually enough.loading (after 100ms; minimum 300ms)prevents a flash of "Loading…" for fast networks. Without these thresholds the placeholder swaps to the loading state too quickly and looks glitchy.@errorcatches chunk-load failures (offline, deploy mid-session). Without it, the user sees nothing and you don't see anything in the console either.
What @defer does NOT do
It is not code splitting for routes — that's still loadComponent and loadChildren on the route definition. It is not a substitute for the bundling decisions in your tsconfig or your angular.json. It does not lazy-load CSS or fonts (Angular puts component styles in the same chunk as the component).
It also doesn't change SEO by itself. If a block has text Google should index, see Angular SSR for SEO — a deferred block renders into HTML on the server but is still client-rendered for indexing purposes, so weigh visibility against bundle weight per block.
A real example
A service detail page with a reviews carousel, a "before/after" slider, and a contact form. Without @defer, all three components are in the initial bundle.
With @defer:
```html <h1>Tree Removal in Newark, NJ</h1> <p>...</p>
<app-contact-form />
@defer (on viewport) { <app-before-after-slider /> } @placeholder { <div style="min-height: 360px"></div> }
@defer (on viewport; prefetch on idle) { <app-reviews-carousel /> } @placeholder { <div style="min-height: 240px"></div> } ```
The reviews block downloads during idle time and is ready by the time the user scrolls. The before/after block stays out of the bundle until the user gets close. Initial download gets measurably smaller with no behavior change.
Common mistakes
- Forgetting
placeholderheight. The lazy chunk arrives and the layout shifts. CLS tanked. Reserve the space every time. - Putting above-the-fold content in
@defer (on viewport). The hero on the home page should not be deferred. If it's visible on first paint, render it normally. - Overusing
@deferfor tiny components. A 3KB button component is not worth the placeholder plumbing. Save@deferfor things that meaningfully change the bundle. - Skipping
@error. A failed chunk load silently disappears. Always include the error block so the UI shows something. - Wrapping the entire route in
@defer. That defeats the SSR story. Routes already lazy-load; use@deferinside them.
The verdict
@defer is the highest-leverage Angular feature most teams underuse. It takes a few lines per block, doesn't change how you write components, and trims a real percentage off the initial bundle. Once you've shipped it on one project, you'll start spotting ten places to add it on the next.
If you want a sanity check on where @defer, @if/@for, signals, and SSR fit together on a real app, send me a message. It's the kind of review that pays back the rest of the project's life.
Want help shipping this on your own site?
Free 30-minute consultation. No pressure either way.
