Deferrable Views with @defer
By the end of this lesson you will be able to make an Angular page faster. You will load its heavy parts later, only when they are needed.
Not everything on a page is needed straight away. A sales chart at the bottom of a dashboard can wait. So can a reviews section behind a button, or a map in the footer. If you load them all at the start, the first download is bigger and the page is slower. That hurts most on a budget phone using 4G in a crowded hostel.
Think of a wedding buffet. The starters come out first, and the desserts arrive later when guests are ready for them. Nobody waits at the door for the dessert counter to be set up.
The @defer block gives Angular a similar instruction: "Split this part into a separate file. Load it later, when a trigger happens." A trigger is the event that starts loading, such as the user scrolling or clicking. The deferred part is called a deferrable view.
A first @defer block
Wrap the component you want to delay in @defer. Then give the user something to look at in the meantime with @placeholder:
import { Component } from '@angular/core';
import { SalesChart } from './sales-chart';
@Component({
selector: 'app-dashboard',
imports: [SalesChart],
template: `
<h2>Canteen dashboard</h2>
@defer (on viewport) {
<app-sales-chart />
} @placeholder {
<div class="chart-box">Chart will load here</div>
}
`,
})
export class Dashboard {}Here is what happens, step by step:
- The page opens. You see only the heading and the placeholder box.
- You scroll down until the placeholder comes into view.
- Angular downloads the chart's code.
- The real chart replaces the placeholder.
SalesChart is used only inside @defer. So the build tool moves it into a separate chunk, a small JavaScript file that loads on its own. Anything that only SalesChart imports goes into that chunk too:
Initial chunk files | Names | Raw size | Estimated transfer size main-C2MRY2L4.js | main | 243.60 kB | 66.66 kB Lazy chunk files | Names | Raw size | Estimated transfer size chunk-WXm2kRVR.js | sales-chart | 776 bytes | 776 bytes chunk-DDJT6CZp.js | reviews | 361 bytes | 361 bytes
This only works when two rules are true:
- The deferred components are standalone.
- They are not used anywhere else in the same component outside
@defer. For example, they must not appear in aviewChildquery or in another part of the template.
If a component is used elsewhere, Angular must load it eagerly (straight away). The block still shows later, but you save no download.
The four blocks
| Block | Shown when |
|---|---|
@defer | The main content, after it has loaded |
@placeholder | Before loading starts (also used as the element for some triggers) |
@loading | While the chunk is downloading |
@error | If loading fails, for example on a lost connection |
On a fast connection, the placeholder and loading message can flash on and off too quickly. That flicker looks broken. @placeholder and @loading accept timing options to prevent it:
@defer (on viewport) {
<app-sales-chart />
} @placeholder (minimum 500ms) {
<div class="chart-box">Chart will load here</div>
} @loading (after 100ms; minimum 1s) {
<p>Loading chart...</p>
} @error {
<p>Could not load the chart. Check your connection.</p>
}@placeholder (minimum 500ms): once the placeholder is shown, keep it for at least 500 ms.@loading (after 100ms; minimum 1s): show the loading message only if loading takes longer than 100 ms. Once shown, keep it for at least 1 second, so it does not blink on and off.
Triggers
The words in brackets after @defer decide when loading starts:
| Trigger | Starts loading when |
|---|---|
on idle | The browser is idle (the default if you give no trigger) |
on viewport | The placeholder (or a given element) enters the visible area |
on interaction | The user clicks or presses a key on the placeholder or a given element |
on hover | The mouse moves over the placeholder or a given element |
on immediate | Right after the rest of the page has rendered |
on timer(3s) | After a delay |
when condition | A condition becomes true, for example when showMap() |
The viewport, interaction and hover triggers watch the placeholder by default. So the placeholder must have exactly one root element (one outer tag).
You can also point these triggers at another element. Use a template reference variable, a name you give an element with #, such as #showReviews:
<button #showReviews>Show reviews</button>
@defer (on interaction(showReviews); prefetch on idle) {
<app-reviews />
} @placeholder {
<p>Click the button to read reviews.</p>
}
@defer (on timer(3s)) {
<p>Offer: 10% off on combos after 3 pm!</p>
}You can list several triggers, separated by semicolons. Loading starts on whichever comes first.
When you use when, prefer a signal, as in when showMap(). In a zoneless app (one without Zone.js watching every change), Angular may not notice a plain field changing. A signal tells Angular when it changes.
Prefetching
Prefetching separates downloading from showing. It is like a Swiggy order that is packed before you open the door. The food is ready, so the hand-over is instant.
In the reviews example above:
prefetch on idledownloads the reviews chunk quietly while the browser is idle.- The reviews stay hidden until the user clicks the button.
- When the user clicks, the code is already there, so the reviews appear instantly.
@defer and server-side rendering
During server-side rendering, the server builds the page's HTML before sending it. For deferred blocks, Angular renders the @placeholder (or nothing), not the deferred content itself.
Hydration is the step where the browser makes server-built HTML interactive. Incremental hydration does this one block at a time. It is on by default in Angular 22 when you use hydration. You can then add hydrate triggers such as @defer (hydrate on viewport):
- The server renders the full content, so the first view is fast and search engines can read it.
- The browser makes that block interactive only when the trigger fires.
When to use @defer
Good candidates:
- Heavy components below the fold: charts, maps, rich text editors, video players.
- Content behind a user action: reviews, comments, a "more details" panel.
- Features most users never open, such as an admin toolbar.
Poor candidates:
- Content visible immediately at the top of the page. Deferring it makes the layout jump and the page feel slower.
- Tiny components. Each chunk is an extra request to the server.
Common mistakes
- Using a deferred component elsewhere in the same template. It is then loaded eagerly, and
@defersaves nothing. - A placeholder with several root elements for
on viewport. Wrap the placeholder content in a single element, or point the trigger at another element. - Deferring above-the-fold content. It delays the most important part of the page.
- Using plain fields with
when. In a zoneless app the change may not be noticed. Use a signal.
Key points
@defersplits the components inside it into a lazy chunk and renders them when a trigger fires.@placeholder,@loadingand@errorcover the time before, during and after a failed load;minimumandafterprevent flicker.- Triggers include
on idle(default),on viewport,on interaction,on hover,on immediate,on timer(...)andwhen. prefetchdownloads code early without showing it, so it appears instantly later.- Deferred components must be standalone and used only inside the
@deferblock to be split out.
Exercise
A college event page has two heavy components. CampusMap sits at the bottom of the page. PhotoGallery should open when the user clicks a "View photos" button. Write the template so that:
- The map loads when it scrolls into view, with a placeholder box until then.
- The gallery loads when the user clicks the button.
- The gallery's code is prefetched when the browser is idle.
- A "Loading photos..." message appears only if loading takes more than 200 ms.
Show solution
<h1>Annual Fest 2026, Jaipur Campus</h1>
<button #viewPhotos>View photos</button>
@defer (on interaction(viewPhotos); prefetch on idle) {
<app-photo-gallery />
} @loading (after 200ms) {
<p>Loading photos...</p>
} @placeholder {
<p>Photos from last year's fest are ready to view.</p>
}
@defer (on viewport) {
<app-campus-map />
} @placeholder {
<div class="map-box">Campus map</div>
}CampusMap and PhotoGallery must appear in the component's imports and nowhere else outside these blocks. Then each gets its own lazy chunk. The single <div> placeholder gives the viewport trigger one element to watch.
Quick Quiz
1. What does @defer do with the components inside it?
Deferrable views split their dependencies into lazy chunks.
2. Which trigger is used if you write @defer { ... } with no trigger?
The default trigger is on idle.
3. What does prefetch on idle add to @defer (on interaction; prefetch on idle)?
Prefetching separates downloading from rendering.
4. Why might @defer fail to create a lazy chunk for a component?
If a deferred dependency is referenced eagerly elsewhere, it cannot be split out.
Learn Angular with a mentor
Free lessons get you started. Our Full Stack Web Development training adds real projects and code reviews from working developers.
- Project-based learning
- Mentor code reviews
- Free counselling call
