Anyone can write a defer block. Knowing what belongs inside one is the actual skill.

The syntax for deferrable views takes about ten minutes to learn. Deciding what should go inside them takes considerably longer, because it is a judgement about your own application rather than a fact about Angular. Defer the wrong thing and you have added a loading state, a second network request and a layout shift in exchange for nothing.

This is a post about the deciding rather than the syntax. What actually belongs behind a defer block, which trigger suits which situation, when prefetching is worth the complexity, and one thing we tried that turned out to be a bad idea.

1. The syntax is the easy part

For completeness, here is the shape of it.

Listing 01A defer block with its companion statesAngular
@defer (on viewport) {
  <app-usage-chart />
} @placeholder {
  <div class="chart-skeleton"></div>
} @loading (minimum 200ms) {
  <app-spinner />
} @error {
  <p>That chart could not be loaded.</p>
}

@placeholder shows before the trigger fires, @loading while the chunk is in flight, and @error if it fails. All three are optional and all three are worth using, because the alternative is content appearing from nowhere and shifting whatever is underneath it.

One thing that has changed since defer blocks arrived: you no longer need standalone: true on the components involved. Standalone is the default now, and the option only exists so you can set it to false when you deliberately want a component available to an NgModule. If you are reading older material on this, that is the first thing that will look out of date.

2. The prerequisite nobody mentions

A defer block only produces a separate chunk if the thing inside it can actually be separated. That sounds obvious and it is where most disappointing results come from.

The component must be standalone, it must live in its own file, and that file must contain nothing else that is imported eagerly from anywhere else. If a constant, a token, an interface or a helper function sits alongside the component and something else in your application imports it directly, the bundler has to include that file eagerly, and your carefully deferred component comes along with it. You will see the defer block working perfectly in the template and no change at all in your build output.

The same trap applies at one remove. If the deferred component imports a shared module that half the application also imports, you have deferred almost nothing. Before deferring anything, look at what it actually pulls in.

If your codebase still has NgModule in it, this is the moment that becomes a real problem, and it is the reason I wrote about the standalone migration the schematic cannot finish separately.

3. Choosing a trigger

The triggers are usually presented as a list, which is not how you choose one. Here is the same information organised by the situation you are actually in.

Content below the fold: on viewport. The default choice, and the one that fits most cases. The block loads when it scrolls into view.

Content behind a user action: on interaction or on hover. A panel that opens, a tab that switches, a menu that expands. on hover buys you a head start on desktop and does nothing useful on touch, so treat it as an optimisation on top of another trigger rather than as the trigger itself.

Genuinely optional extras: on idle. The block loads when the browser has nothing better to do. Good for things that improve the page but that nobody is waiting for.

Something you can express in code: when. A boolean expression, a signal, or anything that resolves to true. Covered below, because it behaves differently from the rest.

on immediate and on timer: rarely on their own. on immediate loads as soon as rendering finishes, which is useful when you want the code out of the initial bundle but do not want to wait for anything. on timer deserves its own section.

4. The timer trap, which the compiler now catches

on timer(3s) looks harmless and is the trigger most likely to make your page worse.

The problem is that a timer has no relationship to what the user is doing. If someone scrolls to your deferred content two seconds in, on timer(3s) makes them wait a further second while the page sits there with a placeholder, for no reason other than that a number was chosen arbitrarily. You have not deferred the work, you have scheduled a delay.

The rule we settled on was never to use a timer as the only trigger, and always to pair it with something responsive to the user. Triggers combine, so this is straightforward:

Listing 02Pairing a timer with something the user controlsAngular
@defer (on viewport; on timer(3s)) {
  <app-related-items />
} @placeholder {
  <div class="related-skeleton"></div>
}

Whichever condition happens first wins, so the timer becomes a backstop rather than a barrier.

The satisfying part is that this is no longer tribal knowledge. Angular v21 added a compiler diagnostic for misconfigured defer triggers, and v22 added a warning when a prefetch trigger is declared without a main trigger. A rule we had to learn by watching a page behave badly is now something the build tells you about.

5. When prefetching is worth it

This is the most transferable idea in the whole topic and the one I had to think about longest.

Prefetch triggers are declared alongside the main triggers and do a different job. The main trigger decides when the component runs. The prefetch trigger decides when its code is downloaded. Separating them lets you have the bundle ready without paying the cost of executing anything.

Listing 03Downloading early, running lateAngular
@defer (on viewport; prefetch on idle) {
  <app-usage-chart />
} @placeholder {
  <div class="chart-skeleton"></div>
}

Whether that separation is worth it comes down to a single question: how expensive is it to run this component, as opposed to download it?

If running it is expensive, because it fires its own requests, does real computation, or initialises a heavy third-party library, then prefetching is exactly right. Get the code onto the device while the browser is idle, and let the component start doing its work only when the user actually needs it.

If running it is cheap, prefetching is complexity you do not need. Just load it with a normal trigger and move on. Two triggers where one would do is a real maintenance cost, and it is one more thing for someone to misread later.

I would add a third case that took us a while to see: if you are not sure, do not prefetch. The failure mode of prefetching everything is that you have recreated your original bundle, delivered slightly later, with more moving parts.

6. Custom when triggers, and their one-way behaviour

The declarative triggers cover most situations, but when takes any boolean expression, which means anything you can compute.

Listing 04Loading a step when the previous one completesAngular
@defer (when checkoutComplete()) {
  <app-order-confirmation />
}

This is how you handle multi-step flows, post-submission states and anything revealed by a calculation crossing a threshold.

The behaviour to know about is that when is one way. Once the expression resolves to true, the block loads, and setting it back to false does not unload anything or return you to the placeholder. It is a latch, not a toggle. If you want content that genuinely comes and goes, @if is the right tool and always was. @defer is about when code arrives, not about what is currently on screen, and confusing the two produces a component that mysteriously refuses to disappear.

7. Something we tried that did not work

We had two variants of an application that shared most of their code but each shipped their own navigation component. Both navigations were ending up in the main bundle for both variants, which is exactly the kind of thing this whole topic is supposed to solve.

The idea was to stop referencing both navigation components statically in the template and instead construct the right one at runtime through ViewContainerRef, so only the one actually needed would be loaded. On paper this is neat. In practice we abandoned it, and the reasons are more interesting than the technique.

Inputs stopped being reactive. A statically declared component gets its input bindings maintained for you. A dynamically created one does not, so we had to push updated values across manually whenever the parent changed. That worked, and it meant reintroducing a lifecycle hook to do a job the template had been doing correctly on its own.

Then a timing bug appeared. On first render the navigation briefly showed a control that should not have been there, which disappeared on the next change detection pass. A visible flicker of the wrong interface, on the most prominent component on the page.

And there was a delay. Because the component was now constructed rather than rendered, there was a perceptible gap before the navigation appeared at all. On a piece of primary navigation that is not an acceptable trade for a smaller bundle.

The thing that decided it was none of those individually. It was that fixing them properly would have taken about as much work as the obvious alternative, which was to decouple the two applications’ navigation properly so each only ever referenced its own. We had reached for a clever runtime mechanism to avoid doing a structural refactor, and the clever mechanism turned out to cost the same as the refactor while also being harder to understand.

8. The other half of defer blocks, which may not apply to you

Everything above is about when code is downloaded. Since v19 that is only half of what a defer block can express, because the same syntax also controls when server-rendered markup is hydrated, using hydrate variants of the triggers. As of v22 incremental hydration is the default behaviour rather than something you opt into.

I want to be straight that we do not use this. The application I have been describing is client-rendered, so there is no server-rendered markup to hydrate and this half of the feature does nothing for us. I am including it because an article about deferrable views that ignores hydration in 2026 would be leaving out something important, and because the question of whether it applies to you has a clear answer.

It applies if you server-render. If you do, the calculation changes in an interesting way: your users already have the markup, so the thing you are deferring is not the content appearing but the JavaScript that makes it interactive. That is usually a bigger win than lazy loading, because the cost you are avoiding is on the main thread rather than on the network. There is also a hydrate never trigger for content that is genuinely static and never needs to become interactive at all, which is a nice thing to be able to say out loud about a footer.

If you do not server-render, none of this is available to you and there is nothing to configure. Worth knowing so you can recognise it in other people’s articles rather than wondering what you are missing.

9. How to decide, in practice

The questions I now ask before deferring anything:

  1. Is it actually separable? Own file, standalone, nothing else importing it eagerly. Check the build output rather than trusting the template.
  2. Does the user need it to understand the page? If yes, do not defer it. A skeleton where the main content should be is worse than the content arriving slightly later.
  3. What is the user doing when it should appear? That answer names your trigger. If you cannot answer it, you have not found the right one yet.
  4. Is running it expensive, or just downloading it? Only the first case justifies a prefetch trigger.
  5. Is there a layout shift? If deferring something makes the page jump, the placeholder needs to reserve the same space.
  6. Would deleting the component be even better? Asked seriously. More than once the honest answer was that nobody needed the thing at all, and removing it beat every deferral strategy available.

That last question is not a joke. The fastest code remains the code you do not ship, and a defer block is a way of postponing work rather than avoiding it.

Sources and further reading