The Angular Mental Model
An integrated framework rather than a view library: components and templates, dependency injection, routing, forms and a build system, shipped and versioned together.
The intent, the obvious build, and why it breaks
Every lesson starts where the work starts: someone wanted an outcome, and the first implementation that comes to mind has a problem.
What does it mean for the framework to supply the whole application structure, not just the rendering?
A team is building an application that will be maintained for years by people who have not met yet. They want the shape of it to be the same in every part of the codebase.
Angular is a view library with more ceremony. Learn the template syntax and the component decorator and you know it, the same way you would know any other framework in this module.
The template syntax is the smallest part. The framework also supplies dependency injection, a router, two form systems, an HTTP layer, a testing harness and a build pipeline — and idiomatic code uses all of them together.
- The template syntax is the smallest part. The framework also supplies dependency injection, a router, two form systems, an HTTP layer, a testing harness and a build pipeline — and idiomatic code uses all of them together.
- Change detection is not the same question as in the other four. A component instance is long-lived, and the framework decides when to re-check its bindings; how it learns that a check is needed is the mechanism that has changed most across versions.
- Dependency injection is not decoration. Where a provider is registered decides the lifetime and the sharing of the thing provided, and that is an application architecture decision expressed in the framework (Who Owns This State?).
- "More ceremony" reads as cost until the codebase is large and shared, at which point the structure being non-negotiable is a substantial part of what a team is buying.
- The bundle starts larger and is reduced by build-time removal of what you do not use. Comparing a starter application against a minimal one from another framework measures the runtime floor, not what you will ship (Bundle Analysis).
What is actually happening
In the browser, not in the framework.
- Components are long-lived instances. A class with a template. The instance persists across updates; the framework re-evaluates its template bindings rather than re-running a function that returns a description.
- Templates are compiled ahead of time into instructions that create and update the view. Bindings are directional by syntax: property binding into the DOM, event binding out of it.
- Change detection walks the component tree, comparing each binding expression with its previous value and updating the DOM where they differ. Marking a component so that its subtree is only checked when its inputs change is the standard way to bound that walk.
- A zone historically supplied the trigger. Patching asynchronous browser APIs let the framework know that something *might* have changed after a timer, an event or a request. It is a broad signal, deliberately: it cannot miss a change, and it cannot tell you which one.
- Signals supply a targeted trigger. A signal read in a template records the dependency, so a write can mark exactly the affected views — the same idea as the fine-grained models, inside a framework that also still has the tree walk (Reactivity Models).
- Dependency injection resolves services by token through a hierarchy of injectors. Root-level providers are application singletons; component-level providers give an instance per component subtree (Dependency Management Without the Container in Backend Engineering teaches the pattern itself).
What this makes the browser do
And which of it is avoidable.
- Loading and parsing a larger framework runtime, reduced by build-time removal of unused code but still the largest floor of the five here (The Real Cost of JavaScript).
- Change detection passes: for each checked component, evaluate each binding expression and compare it with the previous value. Cheap per binding, and proportional to how much of the tree is checked.
- Under the zone-based trigger, a pass can be initiated by any patched asynchronous operation — including a third-party library's timer that has nothing to do with your UI.
- DOM updates where bindings differ, then the browser's usual style, layout, paint and composite work (The Rendering Pipeline).
- Avoidable work: a template expression that calls a function, which is then evaluated on every check rather than when its inputs change (What a Component Costs to Render).
What "integrated" actually means
The other four frameworks in this module answer the question "how does the view update". This one answers that and then keeps going, into how the application is composed, how it navigates, how it collects input, how it talks to a server and how it is built. Whether that is an advantage is a question about your team and your timeline, not about the framework.
The practical consequence is that idiomatic code looks the same across projects. A new engineer joining an Angular codebase already knows where routing lives and how a service is obtained, in a way that is simply not true of an ecosystem-assembled stack.
- Components — long-lived class instances with a template, inputs and outputs. The unit of composition and of change detection (Drawing Component Boundaries).
- Templates — compiled ahead of time, with directional binding syntax for property, event and two-way bindings, plus built-in control flow for conditionals and lists.
- Dependency injection — services resolved by token through an injector hierarchy, where the registration point decides lifetime and sharing (Dependency Management Without the Container).
- Router — nested routes, guards, resolvers and lazily loaded feature areas, mapped onto the history API (Client-Side Routing).
- Forms — two systems: one driven from the template, one constructed in code, both producing validity state you still have to present accessibly (Form State Is a Draft).
- Build system — a first-party CLI that owns compilation, bundling, build-time removal of unused code, and the development server (Bundlers Compared).
Bindings, and the identity requirement hiding in the list syntax
The template syntax is directional on purpose: square brackets push a value into a property, parentheses pull an event out. Once you read it that way, a template stops being punctuation and becomes a description of data flow at a glance.
The detail worth pausing on is the list block's tracking expression. It is required, and it exists for exactly the reason every other framework in this module needs keys: without a stable identity, the framework can only match items by position, and position is not identity (Reconciliation and Keys).
1<!-- [prop] pushes in, (event) pulls out -->2<input3 [value]="query()"4 (input)="onQuery($event)"5 [attr.aria-label]="'Filter orders'" />6 7<!-- Built-in control flow. `track` is required, and it is the8 same identity decision as a key in any other framework. -->9@if (orders().length === 0) {10 <p>No orders match this filter.</p>11} @else {12 <ul>13 @for (order of orders(); track order.id) {14 <li>{{ order.reference }}</li>15 }16 </ul>17}Two things to notice: the binding syntax tells you the direction of every arrow without reading the class, and track order.id is the framework requiring the identity decision rather than letting you default into keying by position.
How much of the tree gets checked
The one performance question that is genuinely specific to this framework is how much of the component tree is examined per pass, and what caused the pass to happen at all. All three answers below are in production use simultaneously across the ecosystem, and they are not interchangeable.
The transition between them is the reason so much Angular advice contradicts other Angular advice. A recommendation written for a zone-based application says something different from one written for a signal-based one, and both are current.
How should this application learn that a view needs updating?
when Existing applications, and code that mutates objects freely. It cannot miss a change, which is genuinely valuable.
cost A pass can be triggered by any patched asynchronous operation, including a third-party timer, and each pass may walk far more of the tree than the change touched.
when Performance-sensitive subtrees where inputs can be treated as immutable references.
cost A contract you can break silently: mutate an input in place and the reference is unchanged, so the view is never checked and goes stale (Immutability as a Concurrency Strategy in Concurrency has the same argument).
when New applications that can adopt the current model throughout, and want targeted updates without the broad trigger.
cost A migration cost across an existing codebase, and an ecosystem where a proportion of libraries still assume a zone is present.
when The integrated stack is not what you need, and the team has no existing investment in it.
cost You give up the consistency that is most of the reason to choose this framework, and take on assembling and maintaining the equivalent yourself (Choosing a Framework).
How to build it
Most important first.
- Use the framework's own answers. The value of an integrated stack is that routing, forms, HTTP and testing are already decided and consistent; assembling a bespoke alternative gives up the reason to be here (Choosing a Frontend Architecture).
- Bound the change-detection walk deliberately. Making components check only when their inputs change turns an application-wide tree walk into a local one, and it is a contract about immutable inputs, not a flag.
- Keep template expressions cheap and free of side effects. They are evaluated on every check, so a function call in a binding is a function call per pass.
- Choose provider scope by lifetime. A service provided at the root outlives every route; one provided on a component dies with it, which is often exactly what a feature's state wants (Who Owns This State?).
- Track list items by a stable identity in the template's list syntax, for exactly the reasons the rest of this module gives (Reconciliation and Keys).
Keyboard, focus, semantics, announcement
A required field on every lesson in this domain, not a section added when there is room.
- Long-lived component instances mean updated bindings write into existing nodes, which preserves focus, selection and node identity across most updates (Node Identity Across Updates).
- Structural directives that add and remove elements do destroy nodes, and a destroyed node that held focus sends focus to the body silently (Focus Management).
- Client-side route changes replace the view without a browser navigation, so nothing is announced and focus stays where it was. The framework provides the pieces; announcing and moving focus after navigation is application work (Client-Side Routing).
- The forms system gives validity state; it does not give an accessible error. Associating the message with the control and announcing it are still yours (Errors People Can Actually Perceive).
- A component library from the framework's ecosystem is not an accessibility guarantee. Test the component you actually ship, with a keyboard (Accessibility Testing).
What can go wrong
- Function calls and object literals in template bindings, evaluated on every check, producing work that scales with change-detection frequency rather than with data changes.
- A third-party library that schedules timers keeping the zone busy, so change detection runs constantly on a page that is doing nothing.
- Input-triggered checking applied to a component that is then handed a mutated object: the reference did not change, so nothing is checked and the view is stale.
- Services provided at the wrong level: state that should have died with a route surviving in a root singleton, which reads to a user as data leaking between sections of the app (Long-Lived Clients and Version Skew).
- Subscriptions that are never unsubscribed, retaining components and their data for the life of the tab (Memory Leaks).
- Asynchronous streams delivering out of order will render whichever arrived last; ordering is the application's job, not the framework's (Out-of-Order Responses).
- A change-detection pass can be triggered while an asynchronous update is in flight, so a template can evaluate against a partially updated model unless the model is updated atomically.
- Interpolated values are escaped, and the framework additionally sanitises values bound into contexts it considers dangerous, such as HTML, style and URL bindings (Cross-Site Scripting).
- The explicit trust API bypasses that sanitisation, which is exactly what it is for and exactly why it is the thing to audit (Sanitization and Trusted HTML).
- Dependency injection is not a security boundary. Any service is reachable from client code, and everything it holds is visible (What the Frontend Is Responsible For in Auth).
- Route guards are navigation logic, not authorization. They decide what the client shows; the server decides what the client may have (Authorization-Aware UI).
- "Angular is heavy." The runtime floor is the largest of the five here, and application growth costs the least. Which number matters depends entirely on the size of what you are building.
- "Zones are how reactivity works." A zone is how the framework learns that something asynchronous happened. It is a trigger, not a dependency graph, and it cannot tell you what changed.
- "OnPush is a performance flag." It is a contract: this component only needs checking when its inputs change by reference. Break the contract by mutating an input and the view goes stale.
- "Dependency injection is a testing convenience." It is the application's composition and lifetime model; testability is a consequence of it, not its purpose.
- "The router handles accessibility." It changes the view. Focus management and announcement after navigation are application code in every framework in this module (Focus Management).
Measuring it, and what changes in the field
- The framework's DevTools include a change-detection profiler that attributes time to individual components across a pass — the direct measurement of an over-broad walk.
- The Performance panel shows the same passes as main-thread scripting, and shows whether they are being triggered by things that are not user interactions (A Mental Model of the Devtools).
- Bundle analysis on a real build, after build-time removal of unused code, is the only meaningful size measurement for this framework (Bundle Analysis).
- On a slow device the runtime floor is felt at load, before any interaction: a larger bundle is more bytes to parse and compile on the least capable device in your population (The Real Cost of JavaScript).
- On a large application the integrated stack is at its best, because the marginal cost of another feature is small and its shape is already decided.
- On a large tree with the default checking strategy, a single event can trigger a walk across far more components than the change affected.
- In a long-lived tab, root-provided services accumulate exactly as long as the tab lives (Long-Lived Clients and Version Skew).
- Structure supplied by the framework buys consistency across a large team and years of maintenance; it costs flexibility and a larger thing to learn before the first useful commit.
- Two change-detection systems coexisting is a genuine transition cost: two mental models, two sets of advice, and code written under both in the same repository.
- A complete first-party stack means fewer decisions and a smaller third-party surface — which is a benefit and a constraint, depending on whether the first-party answer fits your problem.
Where this applies
Frontend advice ages badly and fragments across engines. These labels say what each claim is specific to, and where a different browser, device or framework would differ.
- FRAMEWORK-SPECIFICBeing an integrated stack — router, forms, HTTP, DI and build tooling versioned together — is Angular's distinguishing property. React and Solid ship a view layer and leave the rest to the ecosystem; Vue and Svelte ship official routers and meta-frameworks but keep them as separate, optional packages.
- SPEC-EVOLVINGAngular's change detection is mid-transition: zone-based triggering is being complemented and progressively replaced by signals and zoneless operation, and the build system has been rebuilt on a different bundler. The tree-walk-and-compare model described here remains accurate; check the current release before quoting mechanism details.
- SIMPLIFIEDPresented at the level of "what pieces exist and how they fit". Omitted: the injector hierarchy in detail, the two form systems and their trade-offs, view queries and content projection, and the differences between the standalone and module-based composition styles.
Where the depth lives
This domain teaches the browser-side mechanism and hands the rest off.
- — Software Design — dependency injection, provider scope and module boundaries are general composition concerns; the framework supplies a mechanism for them, not the reasoning behind them.