Angular / Advanced / Lesson 14
Rendering performance and OnPush
Measure the actual rendering bottleneck, use stable reactive identities, and understand OnPush without treating it as a one-time rendering switch.
On this page
Lesson goals
- Explain the update triggers relevant to an OnPush view.
- Separate expensive computation, DOM work, and network cost.
- Validate optimizations against unchanged user behavior.
Identify which work is actually expensive
A slow interaction can come from JavaScript computation, repeated view checks, DOM layout, image loading, network delay, or excessive retained state. Measure a representative interaction and preserve a correct expected result before changing change-detection policy. A local signal calculation does not measure browser layout or input latency. Use appropriate development profiling and production-like conditions without treating one local duration as a universal claim. Record data size, device class, build mode, and the action tested. Angular optimizations help only when they reduce the relevant work while keeping correctness and accessibility intact.
Keep updates visible to the reactive graph
The complete model derives a total from an array signal and replaces the array during append. The first read establishes three; the replacement notifies the dependency graph and the next read produces six. This checks state and derivation, not the speed of a rendered list. In-place mutation can leave a computed value cached when the source equality considers the reference unchanged. Do not use an unrelated click or manual render call to hide that defect. Preserve the intended state update so all consumers receive coherent data regardless of whether a later event happens to trigger checking.
import { computed, signal } from "@angular/core";
const values = signal<readonly number[]>([1, 2]);
const total = computed(() => values().reduce((sum, value) => sum + value, 0));
console.log(total());
values.update(current => [...current, 3]);
console.log(total());
3
6
The totals are three and six. No benchmark duration or browser rendering result is claimed.
OnPush still responds to supported notifications
The contextual component opts into OnPush and reads a signal input. Relevant input changes, events in the subtree, and tracked reactive notifications can cause checking; OnPush does not mean render only once. A new object reference is often important for input updates, while a signal read in the template creates a tracked dependency. The precise update path depends on the applicationโs supported change-detection setup. Do not assume an unobserved external callback automatically schedules the intended view. The fragment needs a host that supplies rows and a real component test to verify visible changes.
import { ChangeDetectionStrategy, Component, input } from "@angular/core";
@Component({
selector: "app-count-view", standalone: true,
changeDetection: ChangeDetectionStrategy.OnPush,
template: `<p>Rows: {{ rows().length }}</p>`
})
export class CountView { readonly rows = input.required<readonly { id: number }[]>(); }
A host providing two rows would display Rows: 2. An actual update test must change the host data and synchronize rendering; it is not executed here.
Reduce the work before adding manual detection controls
Move expensive pure transformations into appropriate memoized derivations, avoid repeated template allocations, keep list keys stable, and limit the rendered set when the feature permits it. Virtualization, deferred views, and route boundaries solve different workload problems and need their own interaction contracts. Manual ChangeDetectorRef operations can be useful at a deliberate integration boundary, but scattered calls often conceal unclear ownership or missed notifications. Do not disable checks broadly to make a benchmark look faster. A smaller amount of rendered work can still be incorrect if focus, announcements, or selected-row state is lost. Test the same user task before and after the optimization.
Keep measurements and regression expectations together
Test unchanged output, interaction, loading and error states, keyboard behavior, and cleanup before accepting timing improvements. A browser trace can show where work occurs, while a unit test protects deterministic calculation; neither replaces the other. Compare cold and warm cases separately and avoid measuring development-only overhead as production behavior. Document the assumptions under which the change helps, including collection size and update frequency. Zone-based and zoneless scheduling have version-specific configuration, so do not promise migration merely by removing a dependency. Follow the baseline documentation and verify actual notifications in a complete fixture before changing application scheduling.
Common errors and fixes
OnPush is described as never checking again.
- Why it happens
- Skipping unaffected work was confused with one-time rendering.
- How to fix it
- Model and test the supported update notifications.
- Prevent it next time
- Verify input, event, and signal paths separately.
Manual detection calls mask in-place state mutation.
- Why it happens
- A missing reactive update was treated as a rendering-speed issue.
- How to fix it
- Fix state replacement and ownership first.
- Prevent it next time
- Test derivations without unrelated UI events.
A faster view loses focus after list updates.
- Why it happens
- Performance was measured without the interaction contract.
- How to fix it
- Preserve stable identity and focus behavior.
- Prevent it next time
- Pair performance evidence with accessibility regressions.
Check your understanding
1. Append zero in the local model instead of three. What should the total be, and why does unchanged derived output not mean the source update failed?
Show worked solution
The total remains three even though the source array gains an element. The source state and derived aggregate represent different facts; an update can be real while one derived value stays equal. Test the array length and total independently. A view showing row count should update to three, while a sum-only view can legitimately show the same number.
2. A developer proposes OnPush for a page dominated by a slow network response. Explain what evidence is needed before claiming the change fixes latency.
Show worked solution
Profile the interaction to separate request delay from client computation and rendering. OnPush can reduce some checking work but cannot make the server response arrive sooner. Preserve request-state and accessibility behavior, measure the relevant client work, and report the actual improvement rather than attributing total latency to change detection without evidence.
Key takeaways
- Optimize measured work while preserving behavior.
- OnPush has update triggers, not a render-once promise.
- Reactive identity and accessibility remain correctness requirements.