10 Years of Angular: From Angular 2 to Signals and Zoneless
On 14 September 2016, the Angular team shipped Angular 2.0 — a ground-up rewrite that broke compatibility with AngularJS and asked a very large community to start over in TypeScript. Many developers were unhappy about it at the time. Ten years and twenty major versions later, Angular is signal-first, zoneless by default, and builds with esbuild and Vite. It has reinvented itself at least twice more, without a second hard break.
This article walks through that decade. We will group the twenty releases into five eras, look at the one problem each era solved, compare the same component written in 2016 style and 2026 style, and be honest about the decisions that did not age well. If you maintain an Angular app of any age, you should come away knowing where your code sits on the timeline and what the path forward looks like.
years of Angular2016 → 2026 · eight milestones
- 2016Angular 2TypeScript, components, RxJS, and a clean break from AngularJS
- 2017Angular 4Version 3 skipped to align the router; HttpClient arrives in 4.3
- 2018Angular 6
ng updateandng addturn upgrades into a command - 2020Angular 9Ivy becomes the default compiler and runtime
- 2022Angular 14 – 15Typed forms, then stable standalone components
- 2023Angular 16 – 17Signals preview,
@if/@for,@defer, esbuild, angular.dev - 2025Angular 21Zoneless and Vitest become the defaults
- 2026Angular 22Signal Forms and Angular Aria stable; majors move to a yearly cycle
First, why "10 years" and not 16? #
AngularJS — the original framework, version 1.x — first shipped in 2010. Angular 2 was not an upgrade of it. It was a new framework with a new language, a new component model, and a new change-detection system. The team kept the name but dropped the "JS" suffix to mark the break, and AngularJS reached end of long-term support on 31 December 2021.
So when people say "Angular" today, they mean the framework that started with version 2. That framework is ten years old this month.
The five eras at a glance #
| Era | Versions | Years | What defined it |
|---|---|---|---|
| The rewrite | 2 – 5 | 2016 – 2017 | TypeScript, components, dependency injection, RxJS, the AngularJS break |
| Tooling maturity | 6 – 8 | 2018 – 2019 | ng update, ng add, Angular Elements, differential loading |
| Ivy | 9 – 13 | 2020 – 2021 | A new compiler and runtime; View Engine and IE11 removed |
| The renaissance | 14 – 17 | 2022 – 2023 | Typed forms, standalone components, signals preview, control flow, esbuild, angular.dev |
| Signals and zoneless | 18 – 22 | 2024 – 2026 | Stable signals, zoneless by default, Signal Forms, Vitest, Angular Aria |
Each era fixed a pain point created or exposed by the one before it. That is the most useful way to read Angular's history.
1. The rewrite (v2 – v5, 2016 – 2017) #
Angular 2 bet on three things that were not mainstream in 2016: TypeScript as the default language, components as the only unit of UI, and RxJS observables as the async primitive. It also shipped a hierarchical dependency-injection system that is still recognisably the same today.
The version numbering got strange quickly. There was no Angular 3. The router package was already at 3.x, so the team jumped to Angular 4 in March 2017 to align every package on one number. Angular 4.3 introduced HttpClient, which replaced the older Http service. Angular 5, in November 2017, added a build optimizer and progressive web app support.
The cost of this era was the NgModule. Every component had to be declared in exactly one module, modules imported other modules, and a single missing import produced an error that pointed somewhere else. It worked, but it was the most common complaint from new developers for the next six years.
2. Tooling maturity (v6 – v8, 2018 – 2019) #
With the core API settled, the team turned to the upgrade experience. Angular 6 (May 2018) introduced ng update and ng add. These commands run migration schematics that rewrite your code as you upgrade, so moving between majors became a command instead of a weekend. The same release introduced Angular Elements for shipping components as custom elements, and providedIn: 'root' for tree-shakable services.
Angular 7 added CDK virtual scrolling and drag and drop. Angular 8 (May 2019) added differential loading, which served modern bundles to modern browsers and legacy bundles to older ones, and switched lazy routes to standard dynamic import(). It also shipped an opt-in preview of a new rendering engine called Ivy.
The lasting legacy of this era is the promise that you upgrade Angular with a tool, not by hand. Every later migration — standalone, control flow, signal inputs — builds on that promise.
3. Ivy (v9 – v13, 2020 – 2021) #
Angular 9, released in February 2020, made Ivy the default compiler and runtime. Ivy compiles each component independently, which made builds faster, bundles smaller, and error messages point at the right file. Ahead-of-time compilation became the default at the same time.
The transition took two and a half years. Libraries had to be republished in the Ivy format, and apps carried a compatibility layer until Angular 13 (November 2021) removed the old View Engine entirely. Angular 12 deprecated Internet Explorer 11, and Angular 13 dropped it.
Ivy did not change much of the syntax you wrote. What it did was make everything that followed possible. Standalone components, signals, and zoneless change detection all depend on a compiler that understands each component in isolation.
4. The renaissance (v14 – v17, 2022 – 2023) #
This is the era where the Angular you write today took shape.
- Angular 14 (June 2022) shipped typed reactive forms and a preview of standalone components — components that declare their own dependencies with no NgModule.
- Angular 15 (November 2022) made standalone stable and added the directive composition API,
NgOptimizedImage, and functional router guards. - Angular 16 (May 2023) introduced signals in developer preview, required inputs, non-destructive hydration, and a preview of the esbuild builder.
- Angular 17 (November 2023) brought the built-in
@if/@for/@switchcontrol flow,@deferfor template-level lazy loading, the esbuild and Vite application builder as the default for new projects, a new logo, and the new documentation site at angular.dev.
The team called it a renaissance at the time, and the label fits. In eighteen months Angular removed its biggest source of boilerplate, gained a fine-grained reactive primitive, and cut build times sharply.
5. Signals and zoneless (v18 – v22, 2024 – 2026) #
The last era finished what the renaissance started.
- Angular 18 (May 2024) added experimental zoneless change detection.
- Angular 19 (November 2024) made standalone the default for components, directives, and pipes, and introduced
linkedSignalandresourceas experimental APIs. - Angular 20 (May 2025) made
effect,linkedSignal, andtoSignalstable, stabilised incremental hydration, and moved zoneless to developer preview. - Angular 21 (November 2025) made zoneless the default for new applications, made Vitest the default test runner, and shipped experimental Signal Forms and a preview of Angular Aria.
- Angular 22 (June 2026) made Signal Forms, asynchronous signals, and Angular Aria stable — covered in detail in our Angular v22 release walkthrough.
Angular 22 also changed the release rhythm. Until v21, a new major shipped every six months. From v22, majors ship every twelve months, with twelve months of active support and twelve months of long-term support. For teams that dreaded the twice-a-year upgrade, that is probably the most welcome change of the decade.
If you want the mechanics behind this era, how the signal graph tracks dependencies and what changes when zoneless is the default cover it in depth.
The same component, ten years apart #
Here is a small user-search component written the way most teams wrote it between 2016 and 2019:
// user-search.component.ts — 2016–2019 style
@Component({
selector: 'app-user-search',
template: `
<input [value]="query" (input)="onQuery($event.target.value)" />
<ul *ngIf="filtered.length; else empty">
<li *ngFor="let user of filtered; trackBy: trackById"
(click)="selected.emit(user)">{{ user.name }}</li>
</ul>
<ng-template #empty><p>No users found.</p></ng-template>
`,
})
export class UserSearchComponent implements OnInit, OnDestroy {
@Input() users: User[] = [];
@Output() selected = new EventEmitter<User>();
query = '';
filtered: User[] = [];
private query$ = new Subject<string>();
private destroy$ = new Subject<void>();
constructor(private analytics: AnalyticsService) {}
ngOnInit() {
this.query$
.pipe(takeUntil(this.destroy$))
.subscribe(q => {
this.query = q;
this.filtered = this.users.filter(u =>
u.name.toLowerCase().includes(q.toLowerCase()));
this.analytics.track('search', q);
});
this.query$.next('');
}
onQuery(q: string) { this.query$.next(q); }
trackById(_: number, u: User) { return u.id; }
ngOnDestroy() { this.destroy$.next(); this.destroy$.complete(); }
}
// …plus a declaration in an NgModule somewhere else.
And the same component in 2026:
// user-search.ts — 2026 style
@Component({
selector: 'app-user-search',
template: `
<input [value]="query()" (input)="query.set($any($event.target).value)" />
@for (user of filtered(); track user.id) {
<li (click)="selected.emit(user)">{{ user.name }}</li>
} @empty {
<p>No users found.</p>
}
`,
})
export class UserSearch {
users = input<User[]>([]);
selected = output<User>();
protected query = signal('');
protected filtered = computed(() =>
this.users().filter(u =>
u.name.toLowerCase().includes(this.query().toLowerCase())));
private analytics = inject(AnalyticsService);
constructor() {
effect(() => this.analytics.track('search', this.query()));
}
}
The differences are worth naming one by one. There is no NgModule, because the component is standalone. @Input() and @Output() became input() and output(), so inputs are signals too. The Subject / takeUntil / ngOnDestroy cleanup disappeared, because computed() derives the list and effect() is cleaned up with the component. *ngIf, *ngFor, trackBy, and the ng-template fallback collapsed into one @for block with a mandatory track and an @empty branch. Nothing here needs zone.js: signal writes tell Angular exactly what to re-render.
Then vs now #
| Concern | 2016 | 2026 |
|---|---|---|
| Unit of organisation | NgModule + declarations | Standalone components |
| Change detection | zone.js patches every async API | Zoneless; signals notify the framework |
| Local state | Class fields + manual markForCheck |
signal(), computed(), linkedSignal() |
| Async data | Observables everywhere | resource() / httpResource(); RxJS for true streams |
| Templates | *ngIf, *ngFor, ngSwitch |
@if, @for, @switch, @defer, @let |
| Forms | Untyped reactive or template-driven | Signal Forms (typed reactive still supported) |
| Build | Webpack | esbuild + Vite |
| Tests | Karma + Jasmine | Vitest |
| Docs | angular.io | angular.dev |
| Releases | Major every 6 months | Major every 12 months |
What didn't age well #
Ten years is long enough for some decisions to look worse in hindsight. None of these are secrets, and the team has addressed most of them.
| Decision | Why it hurt | What replaced it |
|---|---|---|
| Breaking with AngularJS | Large apps had no in-place upgrade; many were rewritten or stayed on 1.x until end of support | A stable API since v2, and ng update migrations for every major since v6 |
| NgModules | Boilerplate, confusing errors, a steep first week for newcomers | Standalone components (stable v15, default v19) |
| zone.js | "Magic" change detection that was hard to debug and ran too often | Signals plus zoneless change detection (default v21) |
| RxJS for everything | Simple state needed BehaviorSubject, subscriptions, and cleanup |
Signals for state; RxJS kept for event streams |
| The long Ivy migration | Two and a half years of dual-format libraries | One compiler; no second engine change since |
::ng-deep |
Deprecated for years without a full replacement | CSS custom properties and deliberate ViewEncapsulation choices |
The pattern is consistent. Angular's early design favoured explicitness and completeness at the cost of ceremony. The last four years have been about keeping the explicitness and cutting the ceremony.
If you're still on an old version #
Versions 2 through 19 no longer receive support. If your app is on one of them, the path is well trodden:
- Open the official update guide at update.angular.dev, pick your current and target versions, and follow the checklist.
- Upgrade one major at a time with
ng update @angular/core @angular/cli, and run your tests between each step. - Once you are on a recent version, adopt the modern APIs with the built-in schematics:
ng generate @angular/core:standalone,@angular/core:control-flow,@angular/core:inject, and@angular/core:signal-input-migration. - Move to zoneless last, after your components read state through signals.
The Modern Angular Tutorial covers every one of these APIs lesson by lesson if you want to relearn the framework from the 2026 baseline.
Conclusion #
The most surprising thing about Angular's decade is not any single feature. It is that the framework made two more fundamental changes — a new compiler, then a new reactivity model — without repeating the AngularJS break. Ivy shipped behind the same syntax. Signals arrived alongside observables rather than instead of them. Standalone components worked next to NgModules for years. The migrations were long, but they were migrations, not rewrites.
The Angular of 2026 is smaller to write, faster to build, and easier to reason about than the Angular of 2016. It is still opinionated and still batteries-included, which remains its main strength and its main cost. If you left Angular during the NgModule years, the framework you come back to is noticeably different.
Here is to the next ten years. Go build something with it.
Try it yourself #
If you have Claude connected to the Angular CLI MCP server (ng mcp), you can ask it to place a legacy component on the timeline and plan the upgrade:
get_best_practicesThis component is written in the 2016–2019 style: it is declared in an NgModule, uses @Input()/@Output(), *ngIf/*ngFor with trackBy, and cleans up a Subject with takeUntil. Against the current best-practices guide I would change four things: (1) make it standalone and remove it from the module (ng g @angular/core:standalone); (2) convert the inputs and outputs to input() and output() (ng g @angular/core:signal-input-migration); (3) replace the template directives with @for / @empty (ng g @angular/core:control-flow); (4) replace the query$ subject with a signal and derive the filtered list with computed() — that removes ngOnInit, ngOnDestroy, and the subscription entirely. Steps 1–3 are mechanical schematics; step 4 is a short manual refactor.Up next in Angular
More from this topic
Enjoyed this article?
Get new Angular tutorials delivered. No spam — just code-first articles when they ship.


