What’s new in Angular 17?

At the beginning of 2023, Sarah Drashner, who as Director of Engineering at Google also heads the Angular team, coined the term Angular Renaissance. This term means a renewal of the framework that has supported us in the development of modern JavaScript solutions for seven years now.

This renewal is incremental and backwards compatible and takes current trends from the world of front-end frameworks into account. This is primarily about developer experience and performance. Standalone Components and Signals are two well-known features that have already emerged as part of this movement.

Angular 17 makes further contributions to the Angular Renaissance: It comes with new syntax for control flow, lazy loading of page parts and better support for SSR. In addition, the CLI now relies on esbuild and thus speeds up the build significantly.

In this article I discuss these innovations using an example application:

Example application

📂 Source Code

New Syntax for Control Flow in Templates

Since its early days, Angular has used structural directives like *ngIf or *ngFor for control flow. Since the control flow needs to be revised anyway to allow for the envisioned fine-grained change detection and for eventually going Zone-less, the Angular team has decided to put it on a new footing. The result is the new build-in control flow, which stands out clearly from the rendered markup:

@for (product of products(); track product.id) {
    <div class="card">
        <h2 class="card-title">{{product.productName}}</h2>
        […]
    </div>
}
@empty {
    <p class="text-lg">No Products found!</p>
}

One thing worth noting here is the new @empty block that Angular renders when the collection being iterated is empty.

Although signals were a driver for this new syntax, they are not a requirement for its use. The new control flow blocks can also be used with classic variables or with observables in conjunction with the async pipe.

The mandatory track expression allows Angular to identify individual elements that have been moved within the iterated collection. This enables Angular (to be more precise: Angular's new reconciliation algorithm) to drastically reduce the rendering effort and to reuse existing DOM nodes. When iterating collections of primitive types, e.g. Arrays with numbers or strings, track could point to the pseudo variable $index according to information from the Angular team:

@for (group of groups(); track $index) {
    <a (click)="groupSelected(group)">{{group}}</a>
    @if (!$last) { 
        <span class="mr-5 ml-5">|</span> 
    }
}

In addition to $index, the other values known from *ngFor are also available via pseudo variables: $count, $first, $last, $even, $odd. If necessary, their values can be stored in template variables too:

@for (group of groups(); track $index; let isLast = $last) {
    <a (click)="groupSelected(group)">{{group}}</a>
    @if (!isLast) { 
        <span class="mr-5 ml-5">|</span> 
    }
}

The new @if simplifies the formulation of else/ else-if branches:

@if (product().discountedPrice && product().discountMinCount) {
    […]
}
@else if (product().discountedPrice && !product().discountMinCount) {
    […]
}
@else {
    […]
}

In addition, different cases can also be distinguished using a @switch:

@switch (mode) {
    @case ('full') {
      […]
    }
    @case ('small') {
      […]
    }
    @default {
      […]
    }
}

Unlike ngSwitch and *ngSwitchCase, the new syntax is type-safe. In the example shown above, the individual @case blocks must have string values, since the mode variable passed to @switch is also a string.

The new control flow syntax reduces the need to use structural directives, which are powerful but sometimes unnecessarily complex. Nevertheless, the framework will continue to support structural directives. On the one hand, there are some valid use cases for it and on the other hand, despite the many exciting innovations, the framework needs to be made backwards compatible.

Automatic Migration to Build-in Control Flow

If you would like to automatically migrate your program code to the new control flow syntax, you will now find a schematic for this in the @angular/core package:

ng g @angular/core:control-flow

Delayed Loading

Typically, not all areas of a page are equally important. A product page is primarily about the product itself. Suggestions for similar products are secondary. However, this changes suddenly as soon as the user scrolls the product suggestions into the visible area of the browser window, the so-called view port.

For particularly performance-critical web applications such as web shops, it makes sense to defer loading less important page parts. This means that the really important elements are available more quickly. Until now, anyone who wanted to implement this idea in Angular had to do it manually. Angular 17 also dramatically simplifies this task with the new @defer block:

@defer (on viewport) {
    <app-recommentations [productGroup]="product().productGroup">
        </app-recommentations>
}
@placeholder {
    <app-ghost-products></app-ghost-products>
}

Using @defer delays the loading of the enclosed page part until a certain event occurs. As a replacement, it presents the placeholder specified under @placeholder. In the demo application used here, ghost elements are first presented for the product suggestions:

Ghost Elements as placeholders

Once loaded, @defer swaps the ghost elements for the actual suggestions:

@defer swaps the placeholder for the lazy-loaded component

In the discussed example, the on viewport event is used. It occurs once the placeholder has been scrolled into view. Besides this event, there are several other options too:

Triggers Description
on idle The browser reports that there are no critical tasks pending (default).
on viewport The placeholder is loaded into the visible area of the page.
on interaction The user begins to interact with the placeholder.
on hover The mouse cursor is moved over the placeholder.
on immediate As soon as possible after the page loads.
on timer(duration) After a certain time, e.g. on timer(5s) to trigger loading after 5 seconds.
when condition Once the specified condition is met, e.g. when (userName !=== null)

By default, on viewport, on interaction, and on hover force a @placeholder block to be specified. Alternatively, they can also refer to other page parts that can be referenced via a template variable:

<h1 #recommentations>Recommentations</h1> 
@defer (on viewport(recommentations)) { 
    <app-recommentations […] />
} 

Additionally, @defer can be told to preload the bundle at an earlier time. As with preloading routes, this approach ensures that bundles are available as soon as you need them:

@defer(on viewport; prefetch on immediate) { […] }

In addition to @placeholder, @defer also offers two other blocks: @loading and @error. Angular displays the former one while it loads the bundle; the latter one is shown in the event of an error. To avoid flickering, @placeholder and @loading can be configured with a minimum display duration. The minimum property sets the desired value:

@defer ( […] ) { 
    […] 
} 
@loading (after 150ms; minimum 150ms) { 
    […] 
} 
@placeholder (minimum 150ms) { 
    […] 
}

The after property also specifies that the loading indicator should only be displayed if loading takes longer than 150 ms.

Build Performance with esbuild

Originally, the Angular CLI used webpack to generate bundles. However, webpack is currently being challenged by newer tools that are easier to use and a lot faster. esbuild is one of these tools that, with over 20,000 downloads per week, has a remarkable distribution.

The CLI team has been working on an esbuild integration for several releases. In Angular 16, this integration was already included as a developer preview. As of Angular 17, this implementation is stable and used by default for new Angular projects via the Application Builder described below.

For existing projects, it is worth considering switching to esbuild. To do this, update the builder entry in angular.json:

"builder" : "@angular-devkit/build-angular:browser-esbuild"

In other words: -esbuild must be added at the end. In most cases, ng serve and ng build should behave as usual, but be a lot faster. The former uses the vite dev server to speed things up by only building npm packages when needed. In addition, the CLI team integrated several additional performance optimizations.

Calling ng build is also drastically accelerated by using esbuild. Factor 2 to 4 is often mentioned as the range.

SSR Without Effort with the new Application Builder

Support for server-side rendering (SSR) has also been drastically simplified with Angular 17. When generating a new project with ng new, a --ssr switch is now available. If this is not used, the CLI asks whether it should set up SSR:

ng new sets up SSR if desired

To enable SSR later, all you need to do is to add the @angular/ssr package:

ng add @angular/ssr

The @angular scope makes clear, this package comes directly from the Angular team. It is the successor to the community project Angular Universal. To directly take SSR into account during ng build and ng serve, the CLI team has provided a new builder. This so-called application builder uses the esbuild integration mentioned above and creates bundles that can be used both in the browser and on the server side.

A call to ng serve also starts a development server, which both renders on the server side and delivers the bundles for operation in the browser. A call to ng build --ssr also takes care of bundles for both worlds as well as building a simple Node.js-based server whose source code uses the schematics mentioned above.

If you can't or don't want to run a Node.js server, you can use ng build --prerender to prerender the individual routes of the application during build.

Further Improvements

In addition to the innovations discussed so far, Angular 17 also brings numerous other rounding features:

  • The router now supports the View Transitions API. This API, offered by some browsers, allows you to animate transitions vis CSS, e.g. when moving from one route to another. This optional feature must be activated when setting up the router using the withViewTransitions function:

    export const appConfig: ApplicationConfig = {
        providers: [
            provideRouter(
                routes,
                withComponentInputBinding(),
    
                // Activating View Transitions API:
                withViewTransitions(),
            ), 
            [...]
        ]
    };
    
    [...]
    
    bootstrapApplication(AppComponent, appConfig)
        .catch((err) => console.error(err));

    For the sake of demonstration, the example uses CSS animations taken from this documentation page.

  • Signals introduced in version 16 as a developer preview are now stable. An important change from version 16 is that Signals are now designed for use with Immutables by default. This makes it easier for Angular to find out where the data structures managed via Signals have changed. To update signals, you can use the set method, which assigns a new value, or the update method, which maps the existing value to a new one. The mutate method has been removed, especially since it does not fit the semantics of Immutables.

    While Signals are out of developer preview now, the effects-Method is still in Developer Preview, as there are still some cases, the Angular team wants to discover more closely.

  • A change to a data-bound Signal makes Angular to just mark the component(s) directly affected by this change (=components that data-bind this Signal) as dirty. This is different from the traditional behavior that also marks all parents as dirty. When used together with OnPush, this brings performance improvements. Also, this is a first step towards the envisioned more fine-grained change detection.

  • There is now a diagnostic that issues a warning if the getter call was forgotten when reading signals in templates (e.g. {{ products }} instead of {{ products() }} ).

  • Animations can now be loaded lazily.

  • By default, the Angular CLI generates standalone components, standalone directives and standalone pipes. ng new also provides bootstrapping of a standalone component by default. This behavior can be deactivated with the --standalone false switch.

  • The ng g interceptor statement generates functional interceptors.

Summary

With version 17, the Angular Renaissance advances. The new syntax for the control flow simplifies the structure of templates. The new reconciliation algorithm used together with the new control flow improves re-rendering performance drastically.

Thanks to deferred loading, less important page areas can be loaded at a later time. This speeds up the initial page load. By using esbuild, the ng build and ng serve statements run noticeably faster. In addition, the CLI now directly supports SSR and prerendering.

More on Modern Angular?

Learn all about Standalone Components in our free eBook:

  • The mental model behind Standalone Components
  • Migration scenarios and compatibility with existing code
  • Standalone Components and the router and lazy loading
  • Standalone Components and Web Components
  • Standalone Components and DI and NGRX

Please find our eBook here:

free ebook

Feel free to download it here now!