In the Angular DevTools Profiler flame graph, what does the Change detection checkbox show, and how does it help confirm that OnPush subtrees are skipped?
answer
- rendered versus actually checked
- grey tiles
- compare against expectations
- the unexpected coloured subtree
basics
~20 sThe Change detection checkbox limits the flame graph highlighting to components that actually went through change detection in the selected cycle, greying out the rest, so an OnPush subtree that should have been skipped but is still coloured stands out.
solid answer
~40 sBy default the flame graph shows the whole rendered tree for a cycle. Ticking **Change detection** highlights only components that were actually checked and greys out those that were not, typically `OnPush` components with no new input, no dirty mark and no changed signal. That turns an assumption into evidence: after an interaction, you expect only the path from the event to the root plus the affected subtrees to be coloured. A coloured subtree that the interaction had no business touching points to its trigger: a parent passing a new object or array reference every check, an `Eager` strategy set explicitly, or a signal it reads that the interaction writes. Since v22 `OnPush` is the default, so an unexpectedly checked subtree is more often a new-reference input than a missing strategy.
go deeper
Recall that the flame graph can be filtered to components that actually went through change detection, with skipped ones in grey.
Explain which components should be checked for an interaction under OnPush and compare that prediction with the filtered flame graph.
Trace an unexpectedly checked subtree to its trigger, usually a new-reference input or a written signal, and verify the fix with a second recording.
Use before-and-after recordings as review evidence for change-detection refactors, rather than accepting OnPush conversions on faith.
## Rendered tree versus checked tree The Angular DevTools **Profiler** records change detection cycles. For a selected cycle, its **flame graph** lays out the element hierarchy vertically and time horizontally. Normally it visualizes the time to *render* the application for that cycle across the whole tree. That default view does not tell you directly which components Angular **checked** and which it **skipped**. With `OnPush` (the default strategy since v22), a component is only checked when: - it receives a new input value from a template binding (compared by reference); - an event is handled inside it or in one of its children; - a signal read in its template changed, or code marked it for check or ran its change detection explicitly. Everything else under an `OnPush` root is skipped for that cycle. The **Change detection** checkbox, above the flame graph, switches the view so that only components which went through change detection are highlighted, and those that did not are shown in grey. ## Using it as a test of your assumptions The workflow is a comparison between what you expect and what the profiler shows: 1. **Predict** the checked set for one interaction. Typing in a search field should check the field's component, its ancestors up to the root, and the components that display the results. 2. **Record** that interaction in the Profiler and select its cycle. 3. **Tick Change detection** and read the flame graph. 4. **Look for coloured subtrees you did not predict.** A sidebar, a footer or a data table lighting up on every keystroke is the lead. ## Why an unrelated subtree might be checked | What you see | Likely cause | Where to look | |---|---|---| | Subtree coloured on every cycle | A parent binds a new object or array each check, for example a template method returning `filter(...)` | The parent's template bindings | | Subtree coloured, component strategy is `Eager` | It is checked whenever the traversal reaches it | The `changeDetection` setting in `@Component` | | Subtree coloured only for some interactions | It reads a signal that those interactions write | Its template's signal reads | | Subtree coloured along with its ancestors | The event was handled inside it | Expected behaviour | A new-reference input is the most common cause in current code. A binding like `[items]="visibleItems()"` where `visibleItems()` is a plain method calling `.filter()` produces a fresh array each check, so the child is never skipped. Replacing it with a `computed()` returns the same array until the source changes. ## A worked example A dashboard has a header with a notification bell, a sidebar of saved views and a main area with a 400-row orders table. Clicking the bell opens a small popover. The prediction: the bell component and its ancestors are checked, plus the popover. The recording, with **Change detection** ticked, shows the bell path coloured as expected, the sidebar grey as expected, and the orders table coloured. Reading the table's parent template reveals `[orders]="openOrders()"`, where `openOrders()` is a method that filters the order list on every call. Each check of the parent hands the table a new array, so the table is checked whenever the parent is, which includes every event in the header. Changing `openOrders` to a `computed()` and recording the same click again shows the table in grey. The bar for that cycle is shorter, and the saved before-and-after profiles document the change. ## What the checkbox does not tell you - It shows **whether** a component was checked, not **why**. The why comes from reading the code with the table above in mind. - A checked component is not necessarily slow. Use the bar chart and tile intensity to see whether the extra checking actually costs time before rewriting anything. - Numbers come from a **development build**, which does extra checking work; the checked or skipped status is what carries over to production, not the absolute milliseconds. ## Where this fits This is the verification step after a change such as extracting a list into its own component or converting inputs to signals. Record the same interaction before and after, save both profiles as JSON, and compare the grey regions. If the grey area did not grow, the refactor did not deliver the skipping you intended.
- The checkbox shows a heavy table component checked on every keystroke in an unrelated input. What is the first thing you check?The table's inputs. If the parent passes a new array or object reference on every check, for example from a template method that filters, the table can never be skipped. Bind a `computed()` or a stable field instead, then record again and confirm the table turns grey.
- Does a component appearing in grey mean it costs nothing in that cycle?For that cycle, Angular did not run its change detection, so its template bindings were not evaluated. Work it does elsewhere, such as timers or DOM listeners outside Angular, is not shown by this view, so a grey tile does not prove the component is free overall.
saying these in an interview costs you the question
- Every component in the flame graph was checked in that cycle
- Grey tiles mean the component threw an error
- An OnPush component is never checked, even when it handles an event
- A checked component is always a performance problem
- The checkbox explains why a component was checked