Celebrate the upcoming Angular 20 release by optimizing performance through template control flow, lazy-loaded components, deferred views, and configuring rendering strategies.
With Angular 20 just around the corner, it’s a good time to revisit some of the features and tooling available in modern Angular. Honoring this upcoming major release number, this four-part series compiles 20 practical recommendations to help you take full advantage of the performance-focused features built into the framework.
Achieving faster runtime performance isn’t just about keeping up with the official release cycle and running ng update @angular/core @angular/cli and other steps from the official update guide. It also requires implementing strategic architectural decisions tailored to your project’s needs.
This first article focuses on rendering optimizations, directly impacting the web browser’s load time, memory usage, and UI responsiveness.
In the coming weeks, we will explore:
Part 2: ReactivityPart 3: Images and Styling EfficiencyPart 4: Build Optimizations and Monitoring
Each part will cover a focused group of recommendations to help you modernize your Angular apps incrementally while delivering measurable performance improvements with each step.
Rendering Performance Recommendations
1. Complete the switch to the new control flow syntax
Angular 20 sets a definitive direction toward the new control flow syntax introduced during the framework’s renaissance period. Back in November 2023, Angular 17 brought, among other decisive features, a solid alternative to the traditional structural directives (*ngIf, *ngFor, *ngSwitch, etc), guiding developers to adopt a more concise syntax for declaring conditionals and loops using @if, @for, and @switch blocks.
Three major versions later, the deprecation of the structural directives is now official, with the intention to remove them entirely in Angular 22.
The Angular Language Service in v20 warns when the legacy structural directives are imported.
Beyond the syntax improvement, this migration unlocks a couple of measurable runtime performance benefits:
The new syntax is compiled entirely at build time, removing the overhead of interpreting structural directives at runtime.The algorithm behind @for blocks enables smarter DOM diffing. Unlike *ngFor, which typically rebuilds DOM nodes unnecessarily, @for uses an identity tracking mechanism to match and reuse nodes efficiently, especially when the track keyword is used, addressing long-standing issues like memory leaks and excessive DOM updates during change detection. The js-framework-benchmark highlights the performance gains (compare the columns angular-cf-* against the angular-ngfor-* ones to check the difference).
In short, this control flow migration is not only about avoiding deprecation warnings from v20. It’s a strategic performance upgrade.
To support developers during this transition, the Angular core provides a generation schematic that rewrites eligible template syntax to use the new control flow blocks, automating much of the migration work. Just make sure to validate the code changes it applies and test your application after running the script using the Angular CLI:
ng generate @angular/core:control-flow
2. Defer non-critical elements
Building on the modernization efforts initiated in earlier versions with the Ivy compiler and the standalone architecture, Angular 17 also introduced the concept of deferrable views as a declarative way to delay the loading and rendering of non-critical content, including Angular components and other HTML elements, until our specific conditions are met.
Here’s a pseudocode example showing how different event triggers and sub-block states can be used:
@defer (<<trigger>>) {
<deferred-component />
<section>Any HTML content</section>
} @placeholder {
<div>Placeholder or content skeleton</div>
} @loading (after <<miliseconds>>; minimum <<miliseconds>>) {
<span>Loading indicator…</span>
} @error {
<p>Corrective actions…</p>
}
Choose the appropriate loading condition, placeholder, indicator, and error state based on technical decisions that balance performance gains with an optimal UX. Here’s a brief explanation of each trigger:
@defer(when booleanFlag) renders the block when a boolean condition becomes true.@defer(on idle) triggers when the browser has completed all critical tasks.@defer(on timer(x)) delays loading by x milliseconds.@defer(on viewport(#element)) triggers when the block itself, or another element marked with a template variable scrolls into the visible area within the browser.@defer(on interaction(#element)) triggers when the user clicks or focuses on the referenced element.@defer(on hover(#element)) triggers on hover.
Under the hood, the Ivy compiler generates dynamic import statements for each component, directive, or pipe used within the @defer block. Then, the corresponding view will be rendered once all dependencies are resolved.
When properly configured, Angular compiles this content into a separate JavaScript chunk that loads only when triggered — ideal for below-the-fold sections or heavier components that shouldn’t be part of the critical rendering path.
To guarantee code splitting, deferred components, directives, or pipes within the deferrable view must be standalone and not referenced outside its block or queried using viewChild. Otherwise, Angular will bundle them in the main file, and the browser will load all of them eagerly.
Note that with ng serve, deferred components may still be bundled together in the main script, as development mode prioritizes fast rebuilds and turns off aggressive code-splitting. Use a production build and check the network activity in Chrome DevTools (or similar tooling) to confirm that the deferred component is loaded as a separate chunk at runtime.
It’s worth mentioning that deferrable content can be downloaded and cached in advance using the prefetch idle or prefetch timer(x) declarations in combination with the regular triggers to reduce latency by the moment the view is set to render.
The deferrable views can also be combined with incremental hydration capabilities, which are stable from Angular 20. In server-side rendering or prerendered scenarios, hydration can be executed via @defer using regular triggers. This process activates prerendered HTML by connecting the resulting nodes to Angular’s runtime logic, enabling interactivity on the client when the specified condition is met.
@defer (hydrate on <<trigger>>) {
<deferred-component />
}
Feel free to pair hydration with regular triggers to control both when the content loads and when it is hydrated on the initial load:
@defer (<<trigger>>; hydrate on <<another trigger>>) {
<deferred-component />
}
If you want the block to be hydrated right away, go with hydrate on immediate. Conversely, use hydrate never to explicitly opt out of hydrating static or non-interactive blocks.
3. Control lazy loading and preloading of components
While @defer offers fine-grained control over when template blocks of a component render, the traditional lazy loading feature from the Angular Router focuses on entire feature areas. Both techniques are complementary and help reduce upfront work for the browser, operating at different layers of the Angular application.
Lazy-loaded routes remain one of the most effective strategies to break a large app into smaller, route-level chunks. With the standalone architecture, you can run the lazy-loaded route generation schematic to automatically configure as many standalone components as possible, detaching them from the main bundle:
ng generate @angular/core:route-lazy-loading
It is still possible to tweak when and how standalone components are preloaded using a custom or third-party preloading strategy — such as QuickLinkStrategy from Minko Gechev or OptInPreloadStrategy, OnDemandPreloadStrategy, or NetworkAwarePreloadStrategy from John Papa. You can base preloading behavior on factors like link visibility, user interaction, network conditions, or any other logic that fits your application’s context.
To further optimize performance, you can experiment with complementing Angular’s built-in capabilities with the Speculation Rules API. This experimental Web API feature allows a JSON-based config file defining navigations or resources to be prefetched or prerendered by the browser.
4. Consider optimizing app rendering with hybrid strategies
In the past two recommendations, we focused on optimizing how the browser handles your application after it loads. But Angular also lets you shift rendering responsibilities to the server with the built-in hybrid rendering system, where each route can be configured to use a specific rendering mode in the server route parameter passed to the provideServerRouting() function to give precise control over how a given route is rendered:
Client (CSR) enables the rendering to happen entirely in the browser after the JavaScript is loaded.Server (SSR) renders the page in the server on each request, delivering to the client a fully formed HTML for faster first paint and better SEO.Prerender (SSG) generates the fully formed HTML at build time, offering speedy static delivery with no server-side computation per request.
Here’s an example showing how to configure these route-level rendering modes:
import { RenderMode, ServerRoute } from ‘@angular/ssr’;
export const serverRouteConfig: ServerRoute[] = [
{ path: ‘cart’, renderMode: RenderMode.Client },
{ path: ‘catalog’, renderMode: RenderMode.Server },
{ path: ‘product/:id’, renderMode: RenderMode.Prerender },
];
Introduced in Angular 19, the ability to resolve route parameters on prerendering finally made it possible to generate static versions of dynamic routes ahead of time, which is ideal for content that doesn’t change very often, such as landing pages or blog posts. For example, to prerender static product pages available via product/:id, now you can use the getPrerenderParams function:
{
path: ‘product/:id’,
renderMode: RenderMode.Prerender,
async getPrerenderParams() {
const catalogService = inject(CatalogService);
const productIds = await catalogService.getProductIds();
return productIds.map(productId => ({ productId }));
}
}
A thoughtful combination of these techniques helps reduce server load, improve SEO, and typically results in faster-perceived performance, especially when paired with the hydration strategies mentioned in the previous recommendation.
5. Reduce component tree size and complexity
Even with lazy loading, deferrable views, and SSR/SSG in place, the cost of rendering DOM subtrees from large Angular components can significantly affect runtime performance. Reducing the size and depth of your components is an effective way to lower memory usage and improve load time. Techniques like content pagination and conditional rendering with @if and @switch blocks, as detailed in recommendation #1, help you keep only the necessary DOM nodes in memory.
Angular CDK’s Scrolling is particularly useful for very long lists, rendering only the elements visible in the viewport. Pair this with @defer for nested child components, and you can drastically shrink the DOM footprint.
If you need to identify unnecessary nesting quickly, Angular DevTools offers a visual inspector of the component tree structure. Optimizing the depth of your component hierarchy will ultimately reduce the computation effort Angular spends traversing and checking deeply nested views at runtime.
Finally, make sure to break large components into smaller units with clear responsibilities and move reusable logic and anything unrelated to direct template binding into Angular services. Tidy components improve testability, and a smaller component tree also makes change detection more efficient, which is a perfect segue to the next optimizations.
What’s next in Part 2: Reactivity
By the time the next article of this series is published, Angular 20.0.0 will be officially released, bringing stable support for many of the modern features introduced over the past few versions.
In the second article, we’ll shift focus to Angular’s reactivity system. We’ll explore the Signals graph for fine-grained updates, Zoneless change detection to eliminate unnecessary Zone.js patching overhead, and how to apply the Resource API to simplify the derivation of asynchronous state. These techniques will reduce unnecessary computation and memory load and improve your overall developer experience.
Stay tuned.
Lee la versión en español de este artículo en el blog de ng-conf:
20 recomendaciones para que tus aplicaciones de Angular funcionen más rápido | Parte 1…
20 Ways to Make Your Angular Apps Run Faster | Part 1: Rendering Optimizations was originally published in ngconf on Medium, where people are continuing the conversation by highlighting and responding to this story.