Back to the homepage
Angular

Angular 17 – Introduction to Angular Renaissance

We’ve finally got it! The stable version of Angular 17 has been released. It seems that the Angular development team sets themselves a goal to surprise us with every new release, and version 17 is no exception. Significant new functionalities and improvements are being introduced. 

Looking at what we’ve received, it’s safe to say that a lot of emphasis has been placed on optimization, especially when it comes to initial bundle size. We also received a new control flow, a solid foundation for signals to become standard in future versions. 

Let’s dive into what Angular version 17 brings and all the benefits its changes offer. 

Angular rebranding 

Just before the arrival of v17, the Angular Renaissance began. An event to showcase the rebranding of Angular. This is the first event of its kind since the beginning of the framework.

The entire branding color palette has been changed, and the red logo has been refreshed, presenting itself with the modern approach that Angular will offer, while keeping the shield shape that has been with us since AngularJS.

We also have a new, updated website https://angular.dev/ where you can find updated versions of the documentation with examples based on the standalone API and the latest techniques, new tutorials that go through the most important parts of Angular, and a playground to try out the framework in the browser.

Built-in Control Flow

One of the biggest and most noticeable changes is the introduction of a new control flow to templates. This change is the first step to moving away from built-in structured directives, whose current design does not allow for working in zoneless applications.

The new syntax is based on a structure called a block. Its appearance significantly differs from what we have seen in templates. We start each block with an @ prefix and then use a syntax very similar to the one we know from JavaScript.

The change affects the three most commonly used structured directives: ngIf, ngSwitch, and ngFor. At this point, there is no planned implementation of custom blocks.

If

Switch

  • In switch instruction, we don’t need to add the break keyword, unlike the standard syntax we know from JS.
Loop

Of all the structures, it is the loop that has received the most valuable improvements:

  • The @empty block is available, which allows us to display the contents when the list we iterate over is empty.
  • It is no longer necessary to create a special trackBy function to pass it as an argument, we only need to specify the unique key of the object to be tracked. Moreover, the @for block requires the use of the track function, which significantly optimizes the process of rendering the list and managing changes without the need for developer interference.

Due to the fact that we have two ways of using the control flow, comes the question: what will happen to the directives we are familiar with? In version 17, they remain unchanged, but with the arrival of future versions and the exit of the new control flow from the developer preview, they will go into a deprecated state. 

However, there is no need to worry about refactoring. The Angular team has created a scheme that automatically migrates the control flow to the new syntax. In most cases, the scheme should handle the transition to the new syntax without interference from the programmer. To switch to the new syntax, all you need to do is execute the following command:

If you are curious why the Angular Team chose this particular syntax and would like to learn more about the exact reasons why the existing directives were not modified, I encourage you to read our article, in which Mateusz answers these questions.

Deferred Loading

Another important new feature is a lazy-loading primitive called defer, based on the syntax introduced in the new control flow. This extremely useful mechanism allows a controlled delay in the loading of selected elements on the page. That is particularly important because by using defer, we can significantly reduce the initial bundle size, which significantly impacts the application’s loading speed, especially for users with weaker Internet connections.

To control when to defer the content of the block, we use predefined conditions: when and on. We can use them individually or in any combination, depending on when we want to load the content.

When

When defines a logical condition that will load the contents of the block when it receives a true value.

An important note is that once the content of a block has been asynchronously loaded, there is no way to undo that loading. If we want to hide the contents of the block, we need to combine the @defer block with the @if block.

On

On allows us to use predefined triggers for which loading will be initiated.

These predefined triggers are :

  • Idle – the default trigger, the element will be loaded when the browser enters the idle state. The requestIdleCallback method is used to determine when the content will be loaded.
  • Interaction – content will be loaded on user interaction i.e.: click, focus, touch, and on input events (keydown, blur, etc.)
  • Immediate – loading will occur immediately after the page has been rendered
  • Timer(x) – content will be loaded after X amount of time
  • Hover – content will be loaded when the user hovers the mouse over the area covered by the functionality, which could be the placeholder content or a passed in element reference.
  • Viewport – content will be loaded when the indicated element appears in the user’s view. The Intersection Observer API is used to detect the visibility of an element.

We also have the ability to combine both conditions and triggers:

It is worth noting that loading content, as in the case of when, is a one-time operation.

Prefetch

There may be situations where we want to separate the process of fetching content from rendering it on the page. In such a case, the prefetch condition comes to the rescue. It allows us to specify the moment (using the previously mentioned triggers) when the necessary dependencies will be downloaded. As a result, interaction with this content becomes much faster, resulting in a better UX.

We also have three very useful, optional blocks we can use inside the </span><b>@defer block.

  • @placeholder – is used to specify the content visible by default until the asynchronously loaded content is activated. Example:

The minimum condition allows you to specify the minimum time after which the delayed content can be loaded. This means that even if the condition is met immediately, the content will be swapped after 2 seconds (in this particular case).

  •  <strong>@loading</strong> – the content of this block is displayed when dependencies are being loaded. Example:

Within this block, we can also use the minimum condition, which works the same way as within the @placeholder. It indicates the minimum time for which the content of the block will be visible. We can also use the “after” condition, which indicates the amount of  time it will take for the block content to appear. If it takes less than 100ms to load, the loader will not appear. Instead, <deferred-cmp /> will appear immediately.

  • <strong>@error</strong> – represents the content that is rendered when the deferred loading failed for some reason. Example:

When using the defer block together with the @error block, it is possible to use a special timeout condition. This condition allows you to set a maximum loading time. If dependencies take longer than the specified time, the contents of the @error block will be displayed. Inside this block, the user has access to the $error variable, which contains information about the error that occurred during the loading process.

Signals

The signals in the developer preview have been with us since v16. If you haven’t had a chance to get acquainted with them yet, I invite you to read Miłosz’s article, which explains how they work in detail.

In Angular 17, signals were released as stable (except for the effect() function, which remained in developer preview), so we can confidently use them in commercial applications. At the end of the developer preview, we received a few significant changes.

Local Change Detection

Undoubtedly one of the most important new features in this version and the first serious step towards signal-based components.

We received the ability to run Change Detection for a single component. This is a great feature that can significantly affect application performance.

In order for Change Detection to work on a per-component basis, 2 requirements must be met:

  • CD must be triggered by a signal
  • All components in our application must have an OnPush strategy
Why does the signal have to be the source of change?

This is because Angular can track which signals are used in the view, so it knows which components should be updated. Based on this property, the way components are marked as dirty when the signal is the source of the change has been changed.

In v16, changing the value of a signal used in the template resulted in the same behavior as with AsyncPipe. The components affected by the change and all their ancestors were marked as dirty. As a result, in the next change detection cycle, all these components were checked again. The process of marking as dirty worked by default using the markViewDirty() method. The exact same function is performed within the markForCheck() method available on the ChangeDetectorRef object.

In the case of v17, if the signal value has been changed, the markAncestorsForTraversal() method is used instead of markViewDirty(), which only sets the component affected by the change as dirty. This has the effect of skipping the ancestors of that component during CD and rendering only a specific view.

Why do all components need to have the OnPush strategy set?

The way Change Detection goes through the component tree has not changed. It is still a process that starts at the root and flies down the tree. Because of this, if some components have the default strategy set, they will always be updated, which is an undesirable effect.

defaultEquals changes

The default way of comparing signal values has been changed. 

The defaultEquals function implementation in v16 considered any two objects to be different so that even if we returned references to the same object, all dependent signals were notified of the change.

implementation of defaultEquals in v16

In v17, the defaultEquals function relies solely on Object.is(). As a result, if an object is mutated by using the update() function without changing the reference, other dependent signals will not be notified of the change.

implementation of defaultEquals in v17

The change may seem confusing at first. However, it can have a positive impact on performance. To ensure that the signal notifies you of the change, use a new instance of the object with the updated properties (e.g., by using the spread operator) or create your own implementation of the equals function and then specify it in the signal options.

Removal of the mutate method

The Angular team decided to remove the mutate function that was previously used to mutate the value stored by the signal. The mutate function skipped comparing values because its purpose was to modify the value of the signal by design. Now, it is recommended to use only the update function.

This is a rather positive change, as it introduces a consistent and predictable way of modifying signals and can help improve the simplicity and quality of code.

Server-side rendering

SSR is an area that the Angular team has been paying a lot of attention to lately, and we know that this will not change with future versions. Already, when we use the ng new command, one of the questions the CLI will ask us is whether we want to enable SSR for our application.

In v17, the default is No, while the plan for v18+ is to add the SSR by default when we generate our application.

Non-destructive hydration left the developer preview

In version 16 of Angular, we got support for non-destructive hydration. Hydration is a mechanism that allows you to render an application on the server and send it to the client for display. 

Non-destructive hydration allows you to add SPA capabilities to the displayed application without destroying the previously rendered DOM tree. This is a more optimal solution than the previous one, which completely replaced the tree rendered on the server. 

In version 17, this feature comes out of the developer preview and is intended for use in production applications.

SSR and deferred loading

For SSR, @defer allows only the content of the @placeholder block to be rendered on the server side. If the @placeholder block is not defined, then all @defer content will remain empty and will be fully rendered on the client side at the time defined by the set triggers.

The plan for the future that we can see in the Angular roadmap is to extend hydration with support for Partial Hydration, which consists of only rendering certain parts of the page or components on the server and sending them to the client, while the rest of the page is rendered on the client side. This solution allows you to take full advantage of deferred loading with SSR.

Improved application builder

The esbuild-based builder has been improved. Until now, it was used experimentally to build artifacts for the browser version of the application (without SSR). In version 17, we have received an improvement, thanks to which we are also able to build the application in SSR and prerender versions. Thanks to the unified solution, we will avoid potential differences resulting from using different bundlers.

Animation’s lazy loading

The problem with loading animations is that until now, they have been downloaded while the application is bootstrapped, even though in most cases, animations occur while the user is interacting with an element on the page. The new functionality in version 17 solves this problem and introduces the ability to load code associated with animations asynchronously. This solution can reduce the size of the main bundle by up to 60 kB.

To start using lazy loading of animations, we need to add provideAnimationsAsync() instead of provideAnimations() to the providers of our application.

And that’s it! Now we just need to make sure that all functions from the @angular/animations module are imported only into dynamically loaded components

As easy as controlling imports in our application is, it can be problematic with the libraries we use. An example of such a library is @angular/material, which relies heavily on @angular/animations, making it very likely that the module responsible for animations will be pulled into the initial bundle.

If we want to check if the animations were downloaded asynchronously, we can build our application with the --named-chunks flag. Then we should see @angular/animations and @angular/animations/browser in separate bundles in the Lazy Chunk Files section.

build with asynchronously loaded animations

build with standard loaded animations

In addition, by using a simple app as an example, we can see that the initial bundle size for an application with asynchronously loaded animations is significantly smaller.

Support for View Transition API

Another important new feature worth mentioning is the introduction of support for the View Transition API. This is a relatively new mechanism that allows us to create smooth and interactive transition effects between different views of a web page. Thanks to this API, we can make changes to the DOM tree while an animation is running between two states.

Adding the View Transition API to our project is very simple.

First, we need to import the withViewTransitions function at the bootstrap point of our application.

And then add it to the provideRouter configuration.

Now, our application immediately gained a subtle input and output effect when changing the URL. Of course, we have the ability to create custom animations. In the example below, the transition effect was extended to 2 seconds by using prepared pseudo-elements in the styles.css file.

This example is a very small sample of what we can achieve with this API. If you are interested in the practical use of this mechanism, I encourage you to read the material available in the Chrome browser documentation.

However, it is important to remember that this is a relatively new and experimental feature. This means that it may not be fully supported in some browsers. You can check the level of support for individual browsers on caniuse.

Other noteworthy changes in Angular 17

  • Applications developed in version 17 will be standalone by default and will use esbuild-based Vite as the default build system.
  • Support for Node 16 has ended. As of Angular 17, the minimum compatible version is Node 18.13.
  • The lowest supported version of TypeScript, as of this release, is 5.2.
  • A small but much appreciated community enhancement is the addition of a styleUrl value to the @Component decorator, which accepts a string with a single path to the style file.
  • Angular DevTools have been enhanced with a new, extremely useful functionality. In the latest update, they give us the ability to view the hierarchy of injectors in our application, which significantly improves DX during the debugging process.

Summary

As we can see, the changes introduced in this version of Angular are significant and will strongly influence how we work with this framework in the future. Migration to the new control flow will eventually become necessary to effectively use signal-based components. In addition, SSR is expected to become a permanent and integral part of Angular. Another important aspect of this release is that many features have been introduced to reduce the load on the application when loading.

What do you think about the direction of Angular? Are you satisfied with the changes made in this version and the rebranding that was done? We look forward to hearing your opinion, which you can share in the comments!

About the author

Piotr Wiórek

Angular developer who loves clean and readable code. In his spare time he plays squash and searches for good ramen.

Don’t miss anything! Subscribe to our newsletter. Stay up-to-date with the latest trends, tips, meetups, courses and be a part of a thriving community. The job market appreciates community members.

Leave a Reply

Your email address will not be published. Required fields are marked *