The New Standard: OnPush as the Default
In Angular 22, OnPush is no longer just an optional performance optimization. It is now the default for new components. This change goes beyond performance and reflects the direction Angular is taking with signals. This shift didn’t happen overnight. It is the final step of a carefully planned transition.
iJS Newsletter
Join the JavaScript community and keep up with the latest news!
The Roadmap to the New Default:
- The Zoneless Foundation: Starting with version 21, the framework enabled zoneless support by default, signaling the end of the zone.js era and the beginning of a lighter, faster runtime.
- The Deprecation of “Default:” In version 21.2, the old ChangeDetectionStrategy.Default was officially marked as deprecated. It was essentially rebranded as the Eager strategy, a new, more descriptive name that better reflects what this mode actually does in Angular.
- The Angular 22 Milestone: Now, in version 22, Angular comes full circle. When you create a new component, OnPush is applied automatically. What used to be a performance-oriented choice is now the default way Angular components work.
What does this mean for your existing projects?
If you’re upgrading an existing codebase, you don’t need to worry about a “breaking change” headache. The Angular team has built an intelligent migration tool into the ng update process that handles the transition automatically.

When you run the update, the migrator automatically scans your components. Any component that relied on the old, implicit Default behavior is updated to ChangeDetectionStrategy.Eager (Listing 1). This ensures your application behaves exactly as it did before, with no hidden changes to the logic.
You are not forced into OnPush for your entire codebase. The migration preserves your existing behavior while bringing your project in line with the new Angular standards.
Listing 1
@Component({
selector: 'app-captain-dashboard',
imports: [
CrewWidgetComponent,
],
templateUrl: './captain-dashboard.component.html',
changeDetection: ChangeDetectionStrategy.Eager,
styleUrl: './captain-dashboard.component.scss',
})
export class CaptainDashboardComponent {
The Stable Milestone: Resource API, Signal Forms & Angular Aria
Angular 22 marks an important step in the maturity of several newer APIs. Resource API, Signal Forms, and Angular Aria are now stable, making them much easier to adopt in production.
- Resource API has come a long way. Introduced in Angular 19 as an experimental feature, it quickly drew a lot of attention from the community. Now, after several iterations and refinements, resource, httpResource, and rxResource are finally stable.
- Signal Forms are also stable now, bringing a more modern approach to handling form states. Angular 22 also includes more detailed documentation and improved compatibility with Angular Material and Angular Aria. One nice improvement worth mentioning is the new debounce option for validateAsync and validateHttp. Previously, debouncing had to be applied at the control level, so it affected all validators attached to that control, including synchronous ones like required. That delayed instant feedback for simple checks just to accommodate async calls.
- Angular Aria is now stable as well, which is especially important for teams building accessible custom components. It gives developers a more reliable foundation for production use.
iJS Newsletter
Join the JavaScript community and keep up with the latest news!
Simplifying Dependency Injection: The @Service Decorator
Angular is making dependency injection a bit more straightforward. The new @Service() decorator is intended to cover most of the cases where we previously used @Injectable({ providedIn: ‘root’ }), giving us a cleaner and simpler way to define services.
Before Angular 22, the CLI generated services with @Injectable({ providedIn: ‘root’ }), as shown in Listing 2. In Angular 22, the same command now uses the new @Service() decorator by default (Listing 3), keeping the same root-provided behavior but with a shorter, more focused syntax.
Listing 2
@Injectable({
providedIn: 'root'
})
export class CatService {
Listing 3
@Service()
export class CatService {
Why the switch?
The biggest win here is getting rid of boilerplate. @Injectable is very flexible, but it exposes options like useValue, useClass, or useExisting that most simple services never need.
@Service is much more opinionated. It is aimed at the common case of a singleton service provided in the root injector. By limiting the configuration surface (you only get a simple factory function if you really need it), it helps keep service definitions straightforward.
On top of that, @Service is designed to be used with the inject() function instead of constructor injection, which keeps dependencies closer to where they are used and makes the class easier to read at a glance. If you try to use constructor injection with @Service, Angular will now fail the build with an error.
Listing 4
@Service()
export class CatService {
constructor(http: HttpClient) {
}
}

Turning off automatic provisioning
By default, @Service() registers the class in the root injector. In some cases, though, you may want the service to exist only within a smaller part of the UI. That is where autoProvided: false comes in (Listing 5). It lets you skip automatic provisioning and add the service manually where you need it.
For example, if a service should live only as long as a specific component is active, you can provide it in that component’s providers array (Listing 6).
Listing 5
@Service({ autoProvided: false })
export class DraftWorkspaceService {
}
Listing 6
@Component({
selector: 'app-draft-editor',
providers: [DraftWorkspaceService],
template: `...`,
})
export class DraftEditorComponent {
protected draftWorkspace = inject(DraftWorkspaceService);
}
If you forget to add the service to the component’s providers array, Angular will fail to resolve it and throw an error, as shown in the screenshot below.

When should you stick with @Injectable?
Use @Injectable() when:
- Your service requires more advanced provider configuration
- You need constructor injection
- The service should live in a non-root scope, such as providedIn: ‘platform’.
In practice, @Service() becomes the new default for simple, root-scoped services, while @Injectable() remains the better tool for advanced DI scenarios. The split is quite clean: use @Service() for the common, simple cases, and switch to @Injectable() when the service needs more flexibility.
Lazy-loaded services with injectAsync()
While @Service() simplifies the common case, Angular now goes one step further with injectAsync(), which is especially useful for lazy-loaded services. Angular has always had a strong dependency injection system, but loading services only on demand has often required more manual work. InjectAsync() makes that much easier by letting Angular resolve a service asynchronously, exactly when it becomes necessary.
This is especially useful when a service depends on a heavy library or supports a feature that is only used occasionally, such as a rich text editor, because you want to keep the initial page load as light as possible. When the service is first requested, the bundler loads a separate JavaScript chunk, and Angular then resolves the service like any other singleton.
Important: Lazy loading works only with auto-provided services, such as @Injectable({ providedIn: ‘root’ }) or @Service().
The example below (Listing 7) shows a dashboard component that loads its chart service asynchronously when it is actually needed. The service is fetched only when showCharts() is called, and only the first call triggers the actual download.
Listing 7
@Component({
selector: 'app-dashboard',
template: `...`,
})
export class DashboardComponent {
private charts = injectAsync(() =>
import('./dashboard-charts.service').then((m) => m.DashboardChartsService)
);
async showCharts() {
const charts = await this.charts();
charts.render();
}
}
If you want to start the download earlier, you can pass a prefetch trigger in the options. By default, it is onIdle (Listing 8), a built-in trigger that waits until the browser becomes idle before starting the load. You are not limited to onIdle, a prefetch trigger can be any function that returns a promise.
Listing 8
export class DashboardComponent {
private charts = injectAsync(() =>
import('./dashboard-charts.service').then((m) =>m.DashboardChartsService),
{ prefetch: onIdle }
);
Browser URL Support for RouterLinks
Angular 22 introduces a small but useful routing improvement. RouterLink now gets a new browserUrl input. It allows you to set a different URL in the browser than the one Angular uses internally for navigation.
This gives you more control over what users see in the address bar. The app can keep its internal route logic unchanged, while the visible URL feels more user-friendly.
Why it matters:
- Cleaner and more readable URLs.
- A visible URL that can differ from the internal route.
- Better support for aliases or alternative navigation paths.
- More flexibility in how navigation is presented to users.
This small API addition gives developers more flexibility when shaping how routes appear to users.
debounced() for Signals
Another interesting addition is debounced(), currently available as an experimental feature. It brings a built-in debounce mechanism directly to signals, filling a noticeable gap in signal-based code.
The Problem It Solves
This is especially relevant in UI patterns like autocomplete or search inputs. When an input changes on every keystroke, you usually do not want to trigger an API call immediately each time. Without debouncing, that can easily lead to too many requests while the user is still typing.
Until now, the usual workaround involved a multi-step loop: converting the signal to an Observable, applying RxJS debounceTime, and then converting the result back into a signal. While it worked, it added unnecessary complexity to something that should feel native.
Powered by the Resource API
What makes debounced() so useful is that it gives you both the debounced value and its state in one place. Instead of returning a plain signal, it returns a Resource, allowing you to track exactly what is happening under the hood:
- loading state: while the timer is running, the resource stays in a loading state and continues to expose the previously settled value.
- resolved state: once the specified delay passes, the resource automatically moves to resolved with the new value.
- errors: if the source signal throws an error, the resource switches to an error state immediately.
Automatic cleanup
Because debounced() runs inside an injection context, Angular handles cleanup automatically. When the injector is destroyed, Angular clears any pending timer and disposes of the resource without any extra code.
Example
In practice, this fits naturally into features like user search or autocomplete.
Listing 9
export class UserSearch {
userQuery = signal('');
debouncedUserQuery = debounced(this.userQuery, 400);
results = httpResource<User[]>(() => {
const search = this.debouncedUserQuery.value();
return `http://localhost:3000/api/users?query=${search}`;
});
}
Here, userQuery updates immediately with every keystroke while debouncedUserQuery settles only after 400ms of inactivity. As a result, httpResource does not fire a new request on every single character.
iJS Newsletter
Join the JavaScript community and keep up with the latest news!
Template comments
Angular now supports both single-line and block comments in templates, which makes it much easier to leave notes right where they matter. That kind of support may sound minor, but it removes one of those tiny everyday annoyances that developers have simply learned to live with. And when a framework takes care of little things like this, the whole developer experience feels smoother.
The example below shows both single-line and block comments in an Angular template.
Listing 10
<div
//Single line comment
class="card">
<h2>User profile</h2>
<button
/* Multi-line
comment
continues here */
type="button">Edit</button>
</div>
WebMCP
Angular’s latest updates are not only about improving the framework’s core developer experience and performance. The framework is also starting to explore how modern web applications can better integrate with AI-powered workflows.
Web applications are becoming increasingly AI-aware, but communication between AI agents and web apps is still limited. Most agents still rely on DOM inspection and inferred behavior, which makes the whole process fragile and harder to trust.
This is the problem WebMCP aims to solve. Introduced by the Chrome team at Google, WebMCP proposes a more structured way for AI agents to communicate with web applications by exposing actions and workflows directly in the browser. Instead of forcing agents to interpret the entire UI, applications can explicitly define what actions are available and how they should be executed.
While the proposal is still experimental, Angular has already introduced early support for WebMCP, making it easier to integrate AI-friendly capabilities directly into Angular applications.
Currently, Angular provides several integration levels for WebMCP:
- Global Availability with Application Scope:
When you want to expose features that should be accessible regardless of where the user is currently navigating, you use the application scope. By registering tools using provideExperimentalWebMcpTools within your app.config.ts (Listing 11), you make those capabilities available to AI agents across the entire lifecycle of the application.
A key advantage here is that the execution of these tools runs within an injection context. This means that inside the execute function, you can directly use inject() to access your services.
Listing 11
export const appConfig: ApplicationConfig = {
providers: [
provideExperimentalWebMcpTools([
{
name: 'searchAdoptionPets',
description: 'Search the pet adoption registry for animals ready for adoption based on species, age group, and behavioral traits.',
inputSchema: {
type: 'object',
properties: {
query: { type: 'string', description: 'Search keywords.' },
species: { type: 'string', enum: ['Dog', 'Cat', 'Rabbit'], description: 'The type of animal.' },
ageGroup: { type: 'string', enum: ['Puppy/Kitten', 'Young', 'Adult', 'Senior'], description: 'The age group of the animal.' },
},
required: ['query', 'species'],
additionalProperties: false,
},
execute: ({ query, species, ageGroup }) => {
const animals = inject(AnimalsService).search(query, species, ageGroup);
return { content: [{ type: 'text', text: JSON.stringify(animals) }] };
},
},
]),
]
};
- Route Scope:
You can also attach WebMCP tools to specific routes by providing provideExperimentalWebMcpTools directly within your route configuration (Listing 12).
However, to ensure this integration behaves predictably, you should configure the router to use withExperimentalAutoCleanupInjectors (Listing 13). This is the key that enables Angular’s automatic cleanup mechanism. When the user navigates away from a route, the framework immediately unregisters the associated AI tools.
Listing 12
export const routes: Routes = [
{
path: 'shelter-management',
loadComponent: () => import('./pages/management/management.component').then((m) => m.ManagementComponent),
providers: [
provideExperimentalWebMcpTools([
{
name: 'calculateWeeklySupplyNeeds',
description: 'Calculates weekly food and medical supply requirements for a specific shelter zone based on current animal occupancy.',
inputSchema: {
type: 'object',
properties: {
sectionName: {
type: 'string',
enum: ['Dog Quarantine', 'Cat Pavilion', 'Small Mammals'],
description: 'The target shelter zone.'
},
includeBufferStock: {
type: 'boolean',
description: 'Set to true if the user asks for an extra safety margin or buffer for new arrivals.',
default: false
}
},
required: ['sectionName'],
additionalProperties: false,
},
execute: ({ sectionName, includeBufferStock }) => {
const inventoryService = inject(ShelterInventoryService);
const report = inventoryService.calculateSupplies(sectionName, includeBufferStock);
return { content: [{ type: 'text', text: JSON.stringify(report) }] };
},
},
]),
],
},
];
Listing 13
import { provideRouter, withExperimentalAutoCleanupInjectors } from '@angular/router';
export const appConfig: ApplicationConfig = {
providers: [
provideRouter(routes, withExperimentalAutoCleanupInjectors()),
]
};
- Service Scope:
You can also bind WebMCP tools directly to individual services using declareExperimentalWebMcpTool. The tool’s lifecycle is tied strictly to the service instance: it becomes available when the service is initialized and is automatically unregistered when the service is destroyed.
Listing 14
@Service()
export class AdoptionApplicationService {
readonly activeApplication = signal<AdoptionApplication | null>(null);
constructor() {
declareExperimentalWebMcpTool({
name: 'getAdoptionApplicationStatus',
description: 'Returns the current evaluation status and pending tasks for the active pet adoption application.',
inputSchema: { type: 'object', properties: {} },
execute: () => ({
content: [{ type: 'text', text: JSON.stringify(this.activeApplication()) }],
}),
});
}
}
- Signal Forms Scope:
This scope bridges the gap between AI assistants and user input by turning standard Angular Signal Forms into intelligent, AI-ready endpoints. Once you register the feature globally via provideExperimentalWebMcpForms (Listing 15), enabling it is as simple as adding the experimentalWebMcpTool property directly to your form definition.
Listing 15
import { provideExperimentalWebMcpForms } from '@angular/forms/signals';
export const appConfig: ApplicationConfig = {
providers: [
provideExperimentalWebMcpForms()
]
};
The biggest advantage of this scope is zero-boilerplate schema generation. Angular inspects your form’s data model and active validators to dynamically present a structured schema to the browser’s AI. The agent can then fill out and submit the entire form programmatically, handling validation errors directly just like a human user would.
Listing 16
readonly volunteerForm = form(
this.model,
(f) => {
required(f.fullName);
required(f.email);
required(f.experience);
minLength(f.experience, 30);
},
{
experimentalWebMcpTool: {
name: 'submitVolunteerApplication',
description: 'Submits an application to become a shelter volunteer. Requires fullName, email, and an experience description of at least 30 characters.',
},
submission: {
action: async (value) => this.volunteerService.registerPendingVolunteer(value),
},
},
);
}
Important: Both WebMCP and Angular’s integration with it are still in an early phase, so some APIs and behaviors may change in future updates.
iJS Newsletter
Join the JavaScript community and keep up with the latest news!
Conclusion
Angular 22 brings a strong mix of stabilization and new ideas, giving developers both a more solid foundation and a few genuinely interesting directions to explore. The official stabilization of the Resource API, Signal Forms, and Angular Aria makes the framework feel more production-ready in the areas that matter most. At the same time, Angular keeps adding features that solve real-world problems more directly, from routing tweaks to signal-based debouncing and new dependency injection capabilities. Of course, these are only some of the highlights. Angular 22 also includes bug fixes and other improvements that help round out the release.
What stands out most is the direction of these changes. Angular is continuing to reduce friction, replace workarounds with native solutions, and make the developer experience feel smoother without losing the framework’s maturity. It is a release that feels less like a reinvention and more like a confident step forward.
Angular keeps proving that maturity does not have to mean stagnation. What are you most excited to try first in Angular 22?
🔍 Frequently Asked Questions (FAQ)
1. What is new in Angular 22?
Angular 22 introduces default OnPush change detection for new components, stable Resource API, stable Signal Forms, and stable Angular Aria. It also adds the new @Service() decorator, injectAsync() for lazy-loaded services, browserUrl support for RouterLink, experimental debounced() signals, template comments, and early WebMCP integration.
2. Is OnPush the default in Angular 22?
Yes, Angular 22 applies OnPush automatically to newly created components. Existing applications are not forced into OnPush because the ng update migration preserves previous behavior by marking components that relied on the old default as ChangeDetectionStrategy.Eager.
3. What happens to existing Angular projects when upgrading to Angular 22?
Existing Angular projects are migrated automatically during ng update. Components that relied on the previous implicit default change detection behavior are updated to ChangeDetectionStrategy.Eager, so the application keeps behaving as it did before the upgrade.
4. Which Angular APIs are stable in Angular 22?
Angular 22 marks the Resource API, Signal Forms, and Angular Aria as stable. This includes stable support for resource, httpResource, and rxResource, along with improved documentation and better compatibility between Signal Forms, Angular Material, and Angular Aria.
5. What is the new @Service() decorator in Angular 22?
The @Service() decorator is a simplified way to define root-provided singleton services. It is intended for common service cases that previously used @Injectable({ providedIn: 'root' }), while @Injectable() remains available for advanced dependency injection scenarios.
6. What is WebMCP support in Angular 22?
Angular 22 introduces early support for WebMCP, an experimental approach for exposing application actions and workflows to AI agents in a structured way. Angular supports WebMCP at application, route, service, and Signal Forms scope, but both WebMCP and Angular’s integration are still in an early phase.




