I Built angular-scan — react-scan for Angular
angular-scan
If you've ever used react-scan, you know the feeling: suddenly your app's rendering behavior is visible. You can see which components re-render, how often, and whether those renders actually changed anything.
Angular never had an equivalent. So I built one.
What is angular-scan?
angular-scan is a lightweight dev-time library that hooks into Angular's internal profiler API to visually highlight every component re-render in real time.
Here's what it shows you:
- Yellow flash — a component was checked and its DOM changed (a legitimate re-render)
- Red flash — a component was checked but its DOM did not change (an unnecessary render)
- Counter badge — a running count of how many times each component has been checked
- Toolbar HUD — a floating panel showing total checks, wasted renders, and a per-component inspector
All of this disappears in production — the entire library is tree-shaken when isDevMode() returns false.
Getting started
Install it:
npm install angular-scan --save-devAdd one line to your app config:
// app.config.ts
import { ApplicationConfig } from '@angular/core';
import { provideAngularScan } from 'angular-scan';
export const appConfig: ApplicationConfig = {
providers: [provideAngularScan()]
};That's it. Serve your app, and you'll immediately see colored flashes wherever Angular runs change detection.
Imperative API
For micro-frontends or apps where you can't modify providers, there's an imperative scan() function:
// main.ts
import { scan } from 'angular-scan';
const stop = scan();
// later...
stop(); // removes overlay and stops trackingConfiguration
provideAngularScan({
enabled: true, // disable entirely (default: true)
flashDurationMs: 500, // flash animation length in ms (default: 500)
showBadges: true, // render count badges on host elements (default: true)
showToolbar: true // floating toolbar HUD (default: true)
});How it works under the hood
This is the part I enjoyed building the most. Here's the approach:
1. Hooking into Angular's profiler
Angular exposes window.ng.ɵsetProfiler() in development mode — the same hook used by Angular DevTools in Chrome. angular-scan registers a profiler callback to intercept every change detection cycle.
const removeProfiler = ng.ɵsetProfiler((event, instance) => {
switch (event) {
case ChangeDetectionStart:
// start observing DOM mutations
break;
case TemplateUpdateStart:
// capture which component instance is being checked
break;
case ChangeDetectionEnd:
// flush and classify renders
break;
}
});2. Classifying renders with MutationObserver
This is the key insight: Angular's profiler tells you which components were checked, but not whether the check changed anything. To distinguish real renders from unnecessary ones, I pair the profiler with a MutationObserver:
ChangeDetectionStart— aMutationObserverbegins recording all DOM mutationsChangeDetectionSyncStart/End— gates which events count as real renders (excludes Angular's dev-modecheckNoChangespass)TemplateUpdateStart— captures the exact component instance being checkedChangeDetectionEnd—MutationObserver.takeRecords()flushes synchronously; each instance is mapped to its host element viang.getHostElement(); components whose subtree had DOM mutations → render, the rest → unnecessary render
3. The canvas overlay
The visual flash effects are drawn on a <canvas> element positioned as position: fixed over the full viewport with pointer-events: none. A requestAnimationFrame loop manages drawing and fading the rectangles over component host elements — no interference with the app's own DOM or event handling.
4. Avoiding infinite loops
A subtle gotcha: the toolbar HUD is itself an Angular component. If its renders were tracked, updating the render count would trigger a new render, which would update the count, which would trigger... you get it.
The solution: the toolbar is created via createComponent() and attached to ApplicationRef outside the normal component tree. The ScannerService holds a reference to the toolbar instance and skips it during profiling:
shouldTrack: (instance) =>
this.config.enabled() && instance !== this.toolbarInstance,5. Signal-safe deferred writes
All Angular signal writes from the profiler callback are wrapped in queueMicrotask() to avoid triggering a new change detection cycle from inside the profiler. This keeps the profiler callback synchronous and predictable while allowing signals to update cleanly on the next microtask.
Interpreting the output
| Signal | Meaning | Common cause |
|---|---|---|
| Yellow flash | DOM changed | Normal update — signal/input changed |
| Red flash | DOM unchanged | Parent uses Default CD; child is OnPush with no changed inputs |
| High wasted count | Checked unnecessarily every tick | Needs OnPush; parent might be Default CD |
| Badge turns red | More unnecessary than necessary renders | Component is OnPush but still gets walked |
Why I built this
Angular's change detection is powerful but opaque. Unlike React where re-renders are explicit (a component function runs again), Angular's CD walks the component tree silently. You only notice a problem when the app gets slow — and by then you're debugging in the dark.
angular-scan makes the invisible visible. You catch performance issues as you develop, not after users complain. It's the kind of feedback loop that changes how you think about component architecture.
Requirements
- Angular ≥ 20
- Development mode only (
ng serve/ng build --configuration development) - The Angular debug APIs (
window.ng) are only available in dev mode —angular-scanis silently disabled otherwise
Works with both zone.js and zoneless Angular applications.
Check it out: angular-scan on GitHub · angular-scan on npm
If you find it useful, I'd love to hear about it. And if it helps you catch an unnecessary render that was killing your frame rate — even better.