Angular v21: What Changed
Angular 21 ships zoneless by default, drops ~37% off your bundle, and finally gives forms the signal treatment. This is the release where Angular’s multi-year reactivity rewrite stops feeling like a work-in-progress and starts feeling like the finished product.
Signal-Based Forms
Reactive forms have been Angular’s biggest pain point for years. Verbose setup, leaky observables, and a ControlValueAccessor abstraction that made custom inputs painful. Signal Forms replace all of that.
The new API is experimental but usable. You define fields with validators, group them into a form, and bind with the [formField] directive. No FormGroup, no FormControl, no valueChanges.subscribe().
const form = new FormGroup({
email: new FormControl('', [
Validators.required,
Validators.email,
]),
name: new FormControl('', [
Validators.required,
]),
});
form.get('email')!.valueChanges
.subscribe(v => console.log(v));
const isValid = form.valid;
const form = new SignalForm({
email: formField('', [
Validators.required,
Validators.email,
]),
name: formField('', [
Validators.required,
]),
});
const emailValue = form.controls.email.value;
const isValid = computed(() => form.valid());
Everything is a signal. form.valid() is reactive. form.controls.email.value() is reactive. No subscriptions to manage, no OnDestroy cleanup.
In the template, binding is one directive:
<input type="email" [formField]="form.controls.email" />
<input type="text" [formField]="form.controls.name" />
<button [disabled]="!form.valid()">Submit</button>
This is a genuine simplification, not just a syntax reshuffle.
Resource API
The resource() API gives you a declarative, signal-native way to handle async data. You define a request signal and a loader. Angular handles the rest — loading states, errors, refetching when the request changes.
const userId = signal(1);
const userResource = resource({
request: userId,
loader: async ({ request: id }) => {
const res = await fetch(`/api/users/${id}`);
return res.json();
},
});
You read the state synchronously in your template: userResource.value() for the data, userResource.status() for loading/error/success. No async pipe. No loading boolean you manually toggle.
If you’re in an RxJS-heavy codebase, rxResource() gives you the same pattern with an Observable-based loader:
const userResource = rxResource({
request: userId,
loader: ({ request: id }) =>
http.get<User>(`/api/users/${id}`),
});
Same reactive behavior, same status signals, but your loader returns an Observable instead of a Promise. This is the migration path for teams that aren’t ready to drop RxJS entirely.
resource() | rxResource() | |
|---|---|---|
| Loader returns | Promise | Observable |
| Best for | New code, simple fetches | Existing RxJS services |
| Cancellation | AbortSignal | Unsubscribe |
| Composability | async/await chains | RxJS operators |
Zoneless Change Detection
Zone.js is gone from new projects. Not deprecated, not optional — gone. ng new no longer includes it. This is the biggest architectural shift in Angular since Ivy.
Previously Angular patched every browser API — setTimeout, fetch, addEventListener — to know when something changed. That magic was convenient but expensive. Zone.js alone added ~30KB to your bundle and triggered change detection on every async event, whether state actually changed or not.
In Angular 21, change detection fires only when a signal updates. You opt in by using signals (which you’re already doing if you follow modern Angular patterns).
For existing projects migrating from v20 or earlier, the switch is one line:
bootstrapApplication(AppComponent, {
providers: [
provideZonelessChangeDetection(),
],
});
Then remove zone.js from your polyfills in angular.json. The migration schematic (ng update) handles most of the mechanical changes. The main thing that breaks: code that relied on Zone.js to trigger change detection after raw setTimeout or Promise callbacks. Those spots need signal() or manual ChangeDetectorRef.markForCheck().
View Transitions API
The router now integrates with the browser’s native View Transitions API. You get smooth cross-fade animations between routes with zero custom animation code.
provideRouter(
routes,
withViewTransitions(),
)
That’s it. The router wraps navigation inside document.startViewTransition(), so the browser handles the animation. You can customize transitions with CSS:
::view-transition-old(root) {
animation: fade-out 0.2s ease-out;
}
::view-transition-new(root) {
animation: fade-in 0.2s ease-in;
}
This is progressive enhancement. Browsers that don’t support the API get normal instant navigation. No polyfill, no fallback logic.
Bundle Size Improvements
| Angular 20 | Angular 21 (zoneless) | Reduction | |
|---|---|---|---|
| main.js | ~180 KB | ~110 KB | ~39% |
| Total | ~245 KB | ~155 KB | ~37% |
The numbers are real. Removing Zone.js drops ~30KB. The rest comes from improved tree-shaking in the esbuild-based build pipeline and more aggressive dead-code elimination in the compiler. Development builds are ~40% faster, production builds ~35% faster.
For mobile users on slow connections, this matters a lot. Smaller bundles mean faster Time to Interactive, better Core Web Vitals, better SEO scores.
What You Should Actually Do
If you’re starting a new project: Use Angular 21. You get zoneless, signal forms, and the resource API out of the box. There’s no reason to start with an older version.
If you’re on Angular 20: Run ng update. Enable zoneless change detection. Start using resource() for new data fetching. Migrate forms to signal forms when you touch them — don’t rewrite everything at once.
If you’re on Angular 18 or 19: Jump to 21. The migration path is smoother than going through each intermediate version. The zoneless schematic and updated ESLint rules catch most issues.
If you’re evaluating Angular vs React/Vue: Angular 21 is the first version in years where the DX argument genuinely favors Angular. Signals + zoneless + signal forms is a coherent, batteries-included reactivity model. No useEffect footguns, no reactivity caveats.
The signal migration is no longer optional. It’s the default. Build accordingly.
Sources:
- Angular Blog — official Angular release announcements