Angular — signals, change detection & SSR
Modern Angular requires two mental models: the classic zone-driven change detection model many applications still run on, and the newer fine-grained signals model. Senior engineers can explain how both affect performance, testing, SSR, and migration.
Core details
Classic change detection: zone.js patches async primitives so Angular can schedule a change-detection pass after async work. This made the framework ergonomic, but broad checks can become expensive when component trees, bindings, or third-party async activity grow.
OnPush: limits checks to input reference changes, events, observable emissions through AsyncPipe, and explicit marks. It improves predictability only when teams respect immutable data and avoid hidden mutation.
Signals: signal, computed, and effect model fine-grained reactivity. A writable signal updates dependents; computed values derive state; effects synchronize with external systems. Signals reduce the need for manual subscription cleanup in many UI paths.
Standalone APIs: bootstrapApplication, standalone components, route-level imports, and functional providers reduce NgModule overhead in greenfield and migration code. They also make route-level code splitting easier to reason about.
SSR and hydration: server-rendered Angular must reconcile with the browser DOM. Hydration bugs come from the same classes as other frameworks: DOM-only APIs during render, non-deterministic output, invalid markup, or directive timing assumptions.
Forms and DI: reactive forms are better for complex validation and explicit state; template-driven forms are lighter for simple cases. Dependency injection scope (providedIn, route providers, component providers) affects lifetime, testability, and accidental shared state.
Understanding
Signals are Angular’s pivot from broad “async happened, check the tree” toward explicit reactive dependency graphs. That does not make RxJS obsolete. RxJS remains strong for streams, cancellation, retries, and async composition; signals are strong for component-local state and derived view values.
Zone-driven apps often struggle because work appears far from the cause. A timer, SDK callback, or WebSocket event can trigger broad checks. The fix might be OnPush, runOutsideAngular, virtualization, signal adoption, or reducing binding work. The correct answer depends on trace evidence.
SSR adds another constraint: the first browser render must match what the server emitted. DOM measurement, storage reads, and viewport-specific branching belong after render or behind stable placeholders.
Practical examples
| Problem | Angular tool | Senior note |
|---|---|---|
| Derived local state | computed | Avoid duplicating derived values in mutable fields |
| External observable | AsyncPipe or RxJS-to-signal bridge | Preserve cancellation and cleanup semantics |
| Hot scroll/resize loop | runOutsideAngular + throttled update | Re-enter Angular only for meaningful UI changes |
| Large table | CDK virtual scroll | Change detection tuning will not save unbounded DOM |
| Complex form | Reactive forms | Model validation, disabled state, and errors explicitly |
Signal-style local state:
const quantity = signal(1);
const price = signal(25);
const total = computed(() => quantity() * price());The template reads total() and Angular can track the dependency.
Senior understanding
| Lens | Speak to |
|---|---|
| Performance | Change detection topology, binding count, virtual scroll, runOutsideAngular, signal invalidation |
| Migration | Incremental NgModule to standalone moves; gradual signals beside RxJS; avoid two competing state models |
| Testing | TestBed for integration, pure service tests where possible, explicit async control with fakeAsync |
| SSR | Hydration-safe rendering, transfer state, browser-only APIs after render |
Trap answers: “Angular is slow” without naming change detection topology; “signals fix everything” without discussing streams, async scheduling, and migration cost.
Practical scenario
For a legacy Angular table with slow typing and polling updates:
- Record whether cost is binding checks, DOM size, filtering, polling frequency, or layout.
- Add
OnPushonly where inputs are immutable and update paths are explicit. - Move high-frequency scroll, resize, or polling loops outside Angular's zone, then re-enter only for meaningful UI changes.
- Use CDK virtual scroll for unbounded rows; change detection tuning does not fix thousands of live DOM nodes.
- Introduce signals for local derived UI state while preserving RxJS for streams, cancellation, retries, and async composition.
This keeps migration incremental instead of creating two competing state systems.
Failure modes
- Mutating input objects under
OnPushand wondering why the view does not update. - Creating effects that write back into their own dependencies.
- Keeping long polling or animation loops inside Angular’s zone.
- Mixing RxJS subscriptions and signals without clear ownership or cleanup.
- Reading
windowduring server render and causing hydration divergence.
Interview drill
Question: "How do Angular signals change the classic zone.js mental model?"
Model answer structure:
- Classic Angular uses zone-patched async boundaries to schedule broad change detection.
OnPushnarrows checks but depends on immutable inputs and explicit marks.- Signals create fine-grained dependency tracking for local state and derived values.
- RxJS remains appropriate for async streams, cancellation, retries, and event composition.
- Migration should be incremental: measure first, tune topology, avoid mixed ownership, and keep SSR/hydration deterministic.
Follow-ups to expect:
- "When should work run outside Angular's zone?"
- "Why can
OnPushfail with hidden mutation?" - "What belongs in a signal versus an observable?"
Diagram
See also
Mark this page when you finish learning it.
Spotted something unclear or wrong on this page?