An Angular dashboard defers a heavy chart below the fold with @defer (on viewport), and fast scrollers see an empty placeholder for a second; how do you fix it?
answer
- the download starts too late
- fetch before it scrolls in
- rootMargin or prefetch on idle
- do not jump to immediate
basics
~20 sAngular's on viewport starts the download only when the placeholder intersects, so the chunk fetch is on the critical path. Start it earlier: add prefetch on idle, or a viewport rootMargin, and give the placeholder the chart's size.
solid answer
~40 sWith `@defer (on viewport)` the chunk request begins when the placeholder enters the viewport, so a user who scrolls quickly waits for the network. I would move the fetch earlier without rendering early: `@defer (on viewport; prefetch on idle)` downloads during idle time, often with `prefetch on idle(2000)` on busy pages since v22, and the chart renders instantly on scroll. Alternatively `on viewport({rootMargin: '400px'})` (v21) triggers before the chart is visible. I would not switch to `on immediate` — it puts the chart's code and render back into startup. I would also size the placeholder to the chart to avoid layout shift, confirm in a production build that the chart really is its own chunk, and measure without HMR, which fetches every defer chunk eagerly in dev.
code
ts · 18 linesimport { Component, signal } from '@angular/core';
import { RevenueChart } from './revenue-chart';
@Component({
selector: 'app-dashboard',
imports: [RevenueChart],
styles: `.chart-skeleton { height: 420px; }`,
template: `
@defer (on viewport({ rootMargin: '400px' }); prefetch on idle(2000)) {
<app-revenue-chart [series]="series()" />
} @placeholder {
<div class="chart-skeleton" aria-hidden="true"></div>
}
`,
})
export class Dashboard {
readonly series = signal<number[]>([]);
}go deeper
Know that on viewport only starts fetching when the placeholder becomes visible.
Explain how prefetch on idle and a viewport rootMargin move the download earlier without rendering early.
Diagnose the gap end to end: trigger timing, chunk presence in a production build, HMR skew, placeholder size and the chart's own data request.
Balance prefetch bandwidth against scroll latency using real usage data, and make that a documented default for heavy widgets.
## Why the gap appears An Angular **`@defer (on viewport)`** block observes its placeholder with an `IntersectionObserver`. Nothing is fetched until the placeholder intersects the viewport. At that moment, three things happen in sequence: 1. Angular starts the dynamic imports for the chart component and its dependencies (often a large charting library). 2. The browser downloads, parses and evaluates the chunk. 3. The chart component is created and renders, usually after its own data request. For a user scrolling slowly this is invisible. For a fast scroller the placeholder is on screen for the whole network round trip — the symptom in the question. ## Options, from least to most eager | Change | Download starts | Render starts | Cost | |---|---|---|---| | `on viewport` (current) | when visible | when loaded | visible wait | | `on viewport({rootMargin: '400px'})` | ~400px before visible | when loaded | still scroll-driven; fast flicks can outrun it | | `on viewport; prefetch on idle` | first idle period | when visible | bytes spent even if never scrolled to | | `on viewport; prefetch on idle(2000)` | idle, or at most 2s | when visible | same, bounded on busy pages (v22) | | `on immediate` | right after first render | right after first render | chart code and render back in the startup path | The usual answer is **`prefetch on idle`**: it separates *download* from *render*, keeps the chart out of the initial bundle and its render out of startup, and removes the network from the scroll path. If the page is busy for a long time after load, the **idle timeout** added in **v22** bounds how long the prefetch can be postponed. `rootMargin` in the viewport options object (added in **v21**) is a good addition, not a replacement: it helps readers who scroll steadily but not someone who drags the scrollbar to the bottom. ## Why not `on immediate` `on immediate` loads the block right after the non-deferred content has rendered. For a heavy chart below the fold, that means: - the chart's chunk competes with above-the-fold resources during startup; - the chart's creation and first render run on the main thread early, when the page most needs to respond; - you have kept a separate chunk but lost the reason to defer. ## The rest of the checklist 1. **Placeholder size.** Give the `@placeholder` root the chart's dimensions (a fixed-height skeleton). Otherwise the page jumps when the chart arrives, and a tiny placeholder can intersect earlier or later than intended. 2. **Is the chunk real?** In a production build, check that the chart has its own lazy chunk. If the chart component is also referenced outside the `@defer` in the same file, it is not a standalone component, or it is imported through a barrel, it stays eager (those rules belong to the placeholder-and-blocks topic, but they are the first thing to rule out). 3. **Measure the right build.** With **HMR** active in `ng serve`, Angular fetches every `@defer` chunk eagerly, so the problem can look absent in development; profile a production build, or serve with `--no-hmr`. 4. **Data, not just code.** If the chart then waits for its own HTTP call, prefetching code does not help. Starting the request earlier (a service call made by the page, cached until the chart asks) is a separate fix. 5. **Low-end devices.** Prefetch spends bandwidth on users who never scroll. If analytics show most users never reach the chart, `rootMargin` alone may be the better trade. ## A final configuration ```html @defer (on viewport({rootMargin: '400px'}); prefetch on idle(2000)) { <app-revenue-chart [series]="series()" /> } @placeholder { <div class="chart-skeleton"></div> } ``` Download during idle time (at most two seconds late), render shortly before the chart scrolls into view, and keep the page layout stable while it waits.
- In development the chart appears instantly, but production shows the gap. Why?With HMR active, Angular fetches every `@defer` block's dependencies eagerly so components can be hot-replaced; rendering still waits for the trigger, but the network wait is gone. Production has no HMR, so the fetch happens on the viewport trigger. Measure a production build, or serve with `--no-hmr`.
- When would you choose rootMargin alone over prefetch on idle?When most users never reach the chart and bandwidth matters, for example on mobile data. `prefetch on idle` downloads for everyone; a viewport `rootMargin` downloads only for users who scroll near it, at the cost of a remaining gap for very fast scrollers.
- The chart code is prefetched but the chart still appears late. What else do you check?Its data. If the chart component fires its HTTP request on creation, the user waits for that call after scrolling. Start the request earlier from the page or a service and let the chart read the cached result, so both code and data are ready at render time.
saying these in an interview costs you the question
- Switching to on immediate is the right fix for any defer delay.
- Prefetch on idle renders the chart early, causing layout work at startup.
- If it looks fine under ng serve with HMR, production timing is the same.
- rootMargin guarantees the chunk is ready for every scroll speed.
- The placeholder's size does not matter for a viewport trigger.