Angular

index


layout: collection override:tags: [] date: 2018-11-05 pathName: angular

Audit your Angular app's accessibility with codelyzer

Making your app accessible means that all users, including those with special needs, can use it and understand the content. According to the World Health Report, more than a billion people—about 15% of the world's population—have some form of disability. So accessibility is a priority for any development project.

In this post you'll learn how to add codelyzer's accessibility checks to the build process for an Angular app. This approach lets you catch accessibility bugs directly in your text editor as you code.

Using codelyzer to detect inaccessible elements

codelyzer is a tool that sits on top of TSLint and checks whether Angular TypeScript projects follow a set of linting rules. Projects set up with the Angular command line interface (CLI) include codelyzer by default.

codelyzer has over 50 rules for checking if an Angular application follows best practices. Of those, there are about 10 rules for enforcing accessibility criteria. To learn about the various accessibility checks provided by codelyzer and their rationales, see the New Accessibility rules in Codelyzer article.

Currently, all the accessibility rules are experimental and disabled by default. You can enable them by adding them to the TSLint configuration file (tslint.json):

{
  "rulesDirectory": [
    "codelyzer"
  ],
  "rules": {
    ...,
    "template-accessibility-alt-text": true,
    "template-accessibility-elements-content": true,
    "template-accessibility-label-for": true,
    "template-accessibility-tabindex-no-positive": true,
    "template-accessibility-table-scope": true,
    "template-accessibility-valid-aria": true,
    "template-click-events-have-key-events": true,
    "template-mouse-events-have-key-events": true,
    "template-no-autofocus": true,
    "template-no-distracting-elements": true
  }
}

TSLint works with all popular text editors and IDEs. To use it with VSCode, install the TSLint plugin. In WebStorm, TSLint is enabled by default. For other editors, check the TSLint README.

With codelyzer's accessibility checks set up, you get a popup showing accessibility errors in TypeScript files or inline templates as you code:

{% Img src="image/admin/XArrTmBXfijqQ8AteI76.png", alt="A screenshot of a codelyzer popup in a text editor.", width="800", height="433" %}
A codelyzer popup showing a form element labeling error.

To perform linting over the entire project (including external templates), use the ng lint command:

{% Img src="image/admin/sZdIj5CNklqppTk0UCf3.png", alt="Linting with Angular CLI", width="800", height="342" %}

Supplementing codelyzer

Lighthouse is another tool you can use to enforce accessibility practices in your Angular application. The main difference between codelyzer and Lighthouse is when their checks get performed. Codelyzer statically analyzes the application at development time, without running it. This means that during development you can get direct feedback in your text editor or in the terminal. By contrast, Lighthouse actually runs your application and performs a bunch of checks using dynamic analysis.

Both tools can be useful parts of your development flow. Lighthouse has better coverage given the checks it performs, while codelyzer allows you to iterate faster by getting constant feedback in your text editor.

Enforcing accessibility checks in your continuous integration

Introducing static accessibility checks in your continuous integration (CI) can be a great enhancement for your development flow. With codelyzer you can easily enforce certain accessibility rules or other practices by running ng lint on each code modification (for example for each new pull request).

This way, even before you proceed to code review, your CI can tell you if there are any accessibility violations.

Conclusion

To improve the accessibility of your Angular app:

  1. Enable the experimental accessibility rules in codelyzer.
  2. Perform accessibility linting over your entire project using the Angular CLI.
  3. Fix all the accessibility problems reported by codelyzer.
  4. Consider using Lighthouse for accessibility audits at runtime.

Create a Progressive Web App with the Angular CLI

In this post, you'll learn how to use the Angular command line interface (CLI) to make a Progressive Web App (PWA).

You can find the code sample from this guide on GitHub.

{% Aside %}

This post assumes you're already familiar with PWAs and their benefits. If you need a refresher, check out the Installable collection.

{% endAside %}

Create an installable PWA

To make your Angular application a PWA, all you need to do is run a single command:

ng add @angular/pwa

This command will:

By default, your service worker should be registered within a few seconds of the first page load. If it isn't, consider modifying the registrationStrategy.

Customize your PWA

The Precaching with the Angular service worker post explains how to configure the Angular service worker. There you can find how to specify which resources you want the service worker to cache and what strategy it should use to do so.

The manifest file of your app lets you specify your app's name, short name, icons, theme color, and other details. Read about the full set of properties you can set on the Add a web app manifest post.

Take a peek at the manifest file generated by the Angular CLI:

{
  "name": "manifest-web-dev",
  "short_name": "manifest-web-dev",
  "theme_color": "#1976d2",
  "background_color": "#fafafa",
  "display": "standalone",
  "scope": "/",
  "start_url": "/",
  "icons": [
    {
      "src": "assets/icons/icon-72x72.png",
      "sizes": "72x72",
      "type": "image/png"
    },
    …
    {
      "src": "assets/icons/icon-512x512.png",
      "sizes": "512x512",
      "type": "image/png"
    }
  ]
}

You can customize any of these properties by changing the relevant value in manifest.webmanifest.

A PWA references its manifest file with a link element in index.html. Once the browser finds the reference, it'll show the Add to Home screen prompt:

{% Img src="image/admin/IgMnFCuRU1Fx9JLZWXuT.png", alt="A progressive web app install prompt", width="344", height="650" %}

Since the ng-add schematics add everything needed to make your app installable, they generate some shortcut icons that are shown once the user adds the app to their desktop:

{% Img src="image/admin/3h7Yuj3MJPiNzbh2xdlB.png", alt="A progressive web app shortcut icon", width="344", height="650" %}

Make sure you customize the manifest properties and icons before you deploy your PWA to production!

{% Aside %} If you want to change your PWA back to a standard web app, just remove the reference to manifest.webmanifest from index.html. {% endAside %}

Conclusion

To make an installable Angular app:

  1. Add @angular/pwa to your project using the Angular CLI.
  2. Edit the options in the manifest.webmanifest file to suit your project.
  3. Update the icons in the src/assets/icons directory to suit your project.
  4. Optionally, edit the theme-color in index.html.

Optimize Angular's change detection

Angular runs its change detection mechanism periodically so that changes to the data model are reflected in an app's view. Change detection can be triggered either manually or through an asynchronous event (for example, a user interaction or an XHR completion).

Change detection is a powerful tool, but if it's run very often, it can trigger a lot of computations and block the main browser thread.

In this post you'll learn how to control and optimize the change detection mechanism by skipping parts of your application and running change detection only when necessary.

Inside Angular's change detection

To understand how Angular's change detection works, let's look at a sample app!

You can find the code for the app in this GitHub repository.

The app lists employees from two departments in a company—sales and R&D—and has two components:

{% Img src="image/admin/JtDFr3VL1e2AyUvhbKVU.png", alt="Sample application", width="800", height="456" %}

You can see the two instances of EmployeeListComponent in the template for AppComponent:

<app-employee-list
  [data]="salesList"
  department="Sales"
  (add)="add(salesList, $event)"
  (remove)="remove(salesList, $event)"
></app-employee-list>

<app-employee-list
  [data]="rndList"
  department="R&D"
  (add)="add(rndList, $event)"
  (remove)="remove(rndList, $event)"
></app-employee-list>

For each employee there's a name and a numeric value. The app passes the employee's numeric value to a business calculation and visualizes the result on screen.

{% Aside %}

For simplicity, the app is using an inefficient version of the fibonacci function as its calculation to show the implications of heavy bindings in an app.

{% endAside %}

Now take a look at EmployeeListComponent:

const fibonacci = (num: number): number => {
  if (num === 1 || num === 2) {
    return 1;
  }
  return fibonacci(num - 1) + fibonacci(num - 2);
};

@Component(...)
export class EmployeeListComponent {
  @Input() data: EmployeeData[];
  @Input() department: string;
  @Output() remove = new EventEmitter<EmployeeData>();
  @Output() add = new EventEmitter<string>();

  label: string;

  handleKey(event: any) {
    if (event.keyCode === 13) {
      this.add.emit(this.label);
      this.label = '';
    }
  }

  calculate(num: number) {
    return fibonacci(num);
  }
}

EmployeeListComponent accepts a list of employees and a department name as inputs. When the user tries to remove or add an employee, the component triggers a corresponding output. The component also defines the calculate method, which implements the business calculation.

Here's the template for EmployeeListComponent:

<h1 title="Department">{% raw %}{{ department }}{% endraw %}</h1>
<mat-form-field>
  <input placeholder="Enter name here" matInput type="text" [(ngModel)]="label" (keydown)="handleKey($event)">
</mat-form-field>
<mat-list>
  <mat-list-item *ngFor="let item of data">
    <h3 matLine title="Name">
      {% raw %}{{ item.label }}{% endraw %}
    </h3>
    <md-chip title="Score" class="mat-chip mat-primary mat-chip-selected" color="primary" selected="true">
      {% raw %}{{ calculate(item.num) }}{% endraw %}
    </md-chip>
  </mat-list-item>
</mat-list>

This code iterates over all the employees in the list and, for each one, renders a list item. It also includes an ngModel directive for two-way data binding between the input and the label property declared in EmployeeListComponent.

With the two instances of EmployeeListComponent, the app forms the following component tree:

{% Img src="image/admin/iMfgq2xaKVRcXc3LhWW7.png", alt="Component tree", width="800", height="454" %}

AppComponent is the root component of the application. Its child components are the two instances of EmployeeListComponent. Each instance has a list of items (E1, E2, etc.) that represent the individual employees in the department.

When the user begins entering the name of a new employee in the input box in anEmployeeListComponent, Angular triggers change detection for the entire component tree starting from AppComponent. This means that while the user is typing in the text input, Angular is repeatedly recalculating the numeric values associated with each employee to verify that they haven't changed since the last check.

To see how slow this can be, open the non-optimized version of the project on StackBlitz and try entering an employee name.

You can verify that the slowdown comes from the fibonacci function by setting up the example project and opening the Performance tab of Chrome DevTools.

{% Instruction 'devtools-performance', 'ol' %}

Now click Record (in the top-left corner of the Performance panel) and start typing in one of the text boxes in the app. In a few seconds, click Record again to stop recording. Once Chrome DevTools processes all the profiling data it collected, you'll see something like this:

{% Img src="image/admin/1rXcCcnM7rLj183jgQCk.png", alt="Performance profiling", width="800", height="276" %}

If there are many employees in the list, this process may block the browser's UI thread and cause frame drops, which leads to a bad user experience.

Skipping component subtrees

When the user is typing in the text input for the sales EmployeeListComponent you know that the data in the R&D department isn't changing—so there's no reason to run change detection on its component. To make sure the R&D instance doesn't trigger change detection, set the changeDetectionStrategy of EmployeeListComponent to OnPush:

import { ChangeDetectionStrategy, ... } from '@angular/core';

@Component({
  selector: 'app-employee-list',
  template: `...`,
  changeDetection: ChangeDetectionStrategy.OnPush,
  styleUrls: ['employee-list.component.css']
})
export class EmployeeListComponent {...}

Now when the user types in a text input, change detection is only triggered for the corresponding department:

{% Img src="image/admin/1hUupNUByRDQLyYYvMcX.png", alt="Change detection in a component subtree", width="800", height="462" %}

You can find this optimization applied to the original application here.

You can read more about the OnPush change detection strategy in the official Angular documentation.

To see the effect of this optimization, enter a new employee in the application on StackBlitz.

Using pure pipes

Even though the change detection strategy for the EmployeeListComponent is now set to OnPush, Angular still recalculates the numeric value for all employees in a department when the user types in the corresponding text input.

To improve this behavior you can take advantage of pure pipes. Both pure and impure pipes accept inputs and return results that can be used in a template. The difference between the two is that a pure pipe will recalculate its result only if it receives a different input from its previous invocation.

Remember that the app calculates a value to display based on the employee's numeric value, invoking the calculate method defined in EmployeeListComponent. If you move the calculation to a pure pipe, Angular will recalculate the pipe expression only when its arguments change. The framework will determine if the arguments of the pipe have changed by performing a reference check. This means that Angular won't perform any recalculations unless the numeric value for an employee is updated.

Here's how to move the business calculation to a pipe called CalculatePipe:

import { Pipe, PipeTransform } from '@angular/core';

const fibonacci = (num: number): number => {
  if (num === 1 || num === 2) {
    return 1;
  }
  return fibonacci(num - 1) + fibonacci(num - 2);
};

@Pipe({
  name: 'calculate'
})
export class CalculatePipe implements PipeTransform {
  transform(val: number) {
    return fibonacci(val);
  }
}

The transform method of the pipe invokes the fibonacci function. Notice that the pipe is pure. Angular will consider all pipes pure unless you specify otherwise.

Finally, update the expression inside of the template for EmployeeListComponent:

<mat-chip-list>
  <md-chip>
    {% raw %}{{ item.num | calculate }}{% endraw %}
  </md-chip>
</mat-chip-list>

That's it! Now when the user types in the text input associated with any department, the app won't recalculate the numeric value for individual employees.

In the app below you can see how much smoother the typing is!

To see the effect of the last optimization try this example on StackBlitz.

The code with the pure pipe optimization of the original application is available here.

Conclusion

When facing runtime slowdowns in an Angular app:

  1. Profile the application with Chrome DevTools to see where the slowdowns are coming from.
  2. Introduce the OnPush change detection strategy to prune a component's subtrees.
  3. Move heavy computations to pure pipes to allow the framework to perform caching of the computed values.

Get started: optimize an Angular application

Angular is a framework for building user interfaces. It provides building blocks to help you quickly set up a maintainable, scalable application. Angular empowers developers to create applications that live on the web, mobile, or the desktop.

What's in this collection?

This collection focuses on five major areas for optimizing an Angular application:

Each post in the collection will describe techniques that you can directly apply to your own applications.

What's not in this collection?

This collection assumes that you're already familiar with Angular and TypeScript. If you're not feeling confident with them yet, check out the TypeScript documentation and the Getting Started with Angular guide on angular.io.

Start a project

The Angular command line interface (CLI) lets you quickly set up a simple client-side Angular application. This post has a short introduction to the CLI, while other posts in the collection show how to add more advanced features like server-side rendering and deployment support.

Set up the CLI

To begin, install the CLI globally and verify that you have the latest version by running these commands:

npm i -g @angular/cli
ng --version

Make sure the last command outputs version 8.0.3 or newer.

Alternatively, if you don't want to install the CLI globally, you can install it locally and run it with the npx command:

npm i @angular/cli
npx ng --version

Create the project

To create a new project run:

ng new my-app

This command will create the initial files and folder structure for your application and install the node modules it needs.

Once the setup process completes successfully, start your application by running:

cd my-app
ng serve

You should now be able to access your application at http://localhost:4200.

What's next?

In the rest of this collection you'll learn how to improve the performance, accessibility, and SEO of your Angular application. Here's what's covered:

Performance budgets with the Angular CLI

Optimizing an Angular application is important, but how do you make sure its performance doesn't regress over time? By introducing performance metrics and monitoring them on each code change!

One important metric is the size of the JavaScript shipped with your application. By introducing a performance budget that you monitor on each build or pull request, you can make sure your optimizations persist over time.

Calculate your performance budget

You can use this online budget calculator to estimate how much JavaScript your app can afford to load, depending on the Time to Interactive you're aiming for.

{% Img src="image/admin/TWPRBRI7ja8d33unYYK6.png", alt="Budget calculator", width="800", height="524" %}

Configure a performance budget in the Angular CLI

Once you have a target JavaScript budget, you can enforce it using the Angular command line interface (CLI). To see how that works, check out this sample app on GitHub.

You'll see that the following budget has been configured in angular.json:

"budgets": [{
  "type": "bundle",
  "name": "main",
  "maximumWarning": "170kb",
  "maximumError": "250kb"
}]

Here's a summary of what's being specified:

Now try building the app by running ng build --prod.

You should see this error in the console:

{% Img src="image/admin/KXJS3kX1XGnItcrS8HJS.png", alt="Budget failure", width="800", height="258" %}

To fix the build error, take a look at app.component.ts, which includes an import from rxjs/internal/operators. This is a private import that's not supposed to be used by consumers of rxjs. It increases the bundle size a lot! When you update to the correct import, rxjs/operators, and run the build again, you'll see that it passes the budget check successfully.

Note that, since differential loading is enabled by default in the Angular CLI, the ng build command produces two builds of the app:

The index.html file of the sample app refers to both builds so that modern browsers can take advantage of the smaller ECMAScript 2015 build and older browsers can fall back to the ECMAScript 5 build.

Enforce your budget as part of continuous integration

Continuous integration (CI) offers a convenient way to monitor the budget of your app over time. And, luckily, the quickest way to set that up is to build your app with the Angular CLI—no extra steps required! Whenever the JavaScript bundle exceeds the budget, the process will exit with code 1, and the build will fail.

If you prefer, you can also enforce a performance budget using bundlesize and Lighthouse. The main difference between performance budgets in the Angular CLI and Lighthouse is when the checks get performed. The Angular CLI performs the checks at build time, looking at the production assets and verifying their sizes. Lighthouse, however, opens the deployed version of the application and measures the asset size. Both approaches have their pros and cons. The check that Angular CLI performs is less robust but much faster since it's a single disk lookup. On the other hand, the LightWallet of Lighthouse can perform a very accurate check by considering dynamically loaded resources, but it needs to deploy and open the app each time it runs.

bundlesize is quite similar to the Angular CLI's budget check; the main difference is that bundlesize can show the check results directly in GitHub's user interface.

Conclusion

Establish performance budgets with the Angular CLI to make sure your Angular app's performance doesn't regress over time:

  1. Set a baseline for the resource size either by using a budget calculator or by following your organization's practices.
  2. Configure size budgets in angular.json under projects.[PROJECT-NAME].architect.build.configurations.production.budgets
  3. The budgets will be automatically enforced on each build with the Angular CLI.
  4. Consider introducing budget monitoring as part of continuous integration (which can also be achieved with the Angular CLI).

Precaching with the Angular service worker

When users have limited network access—or none at all—web app functionality can significantly degrade and often fails. Using a service worker to provide precaching lets you intercept network requests and deliver responses directly from a local cache instead of retrieving them from the network. Once your app's assets have been cached, this approach can really speed up an app and make it work when the user is offline.

This post walks through how to set up precaching in an Angular app. It assumes you're already familiar with precaching and service workers in general. If you need a refresher, check out the Service workers and the Cache Storage API post.

{% Aside %}

You can find the code for the current example on GitHub.

{% endAside %}

Introducing the Angular service worker

The Angular team offers a service worker module with precaching functionality that's well integrated with the framework and the Angular command line interface (CLI).

To add the service worker, run this command in the CLI:

ng add @angular/pwa

{% Aside %}

If you have multiple projects in the Angular CLI workspace, you can optionally specify a --project property with the project name you want to add the service worker to.

{% endAside %}

@angular/service-worker and @angular/pwa should now be installed in the app and should appear in package.json. The ng-add schematic also adds a file called ngsw-config.json to the project, which you can use to configure the service worker. (The file includes a default configuration that you'll customize a little later.)

Now build the project for production:

ng build --prod

Inside the dist/service-worker-web-dev directory you'll find a file called ngsw.json. This file tells the Angular service worker how to cache the assets in the app. The file is generated during the build process based on the configuration (ngsw-config.json) and the assets produced at build time.

Now start an HTTP server in the directory containing your app's production assets, open the public URL, and check out its network requests in Chrome DevTools:

{% Instruction 'devtools-network', 'ol' %}

Note that the network tab has a bunch of static assets directly downloaded in the background by the ngsw-worker.js script:

{% Img src="image/admin/XL0o6p4YbQiBJmWW8Kw4.png", alt="Sample app", width="800", height="599" %}

This is the Angular service worker precaching the static assets specified in the generated ngsw.json manifest file.

One important asset is missing though: nyan.png. To precache this image you need to add a pattern that includes it to ngsw-config.json, which lives in the root of the workspace:

{
  "$schema": "./node_modules/@angular/service-worker/config/schema.json",
  "index": "/index.html",
  "assetGroups": [
    {
      "name": "app",
      "installMode": "prefetch",
      "resources": {
      "files": [
        "/favicon.ico",
        "/index.html",
        "/*.css",
        "/*.js",
        "/assets/*.png"
        ]
      }
    },
    ...
}

This change adds all PNG images in the /assets folder to the app resource asset group. Since the installMode for this asset group is set to prefetch, the service worker will precache all the specified assets—which now include PNG images.

Specifying other assets to be precached is just as straightforward: update the patterns in the app resource asset group.

Conclusion

Using a service worker for precaching can improve the performance of your apps by saving assets to a local cache, which makes them more reliable on poor networks. To use precaching with Angular and the Angular CLI:

  1. Add the @angular/pwa package to your project.
  2. Control what the service worker caches by editing ngsw-config.json.

Route-level code splitting in Angular

This post explains how to set up route-level code splitting in an Angular application, which can reduce JavaScript bundle size and dramatically improve Time to Interactive.

You can find the code samples from this article on GitHub. The eager routing example is available in the eager branch. The route-level code splitting example is in the lazy branch.

{% Aside %} This post assumes understanding of the Angular router. For a guide on how to use it, visit Angular's official documentation. {% endAside %}

Why code splitting matters

The ever growing complexity of web applications has led to a significant increase in the amount of JavaScript shipped to users. Large JavaScript files can noticeably delay interactivity, so it can be a costly resource, especially on mobile.

The most efficient way to shrink JavaScript bundles without sacrificing features in your applications is to introduce aggressive code splitting.

Code splitting lets you divide the JavaScript of your application into multiple chunks associated with different routes or features. This approach only sends users the JavaScript they need during the initial application load, keeping load times low.

{% Aside 'note' %}

By using code splitting, Twitter and Tinder observed improvements of up to 50% for their Time to Interactive.

{% endAside %}

Code splitting techniques

Code splitting can be done at two levels: the component level and the route level.

This post focuses on setting up route-level splitting in Angular.

Sample application

Before digging into how to use route level code splitting in Angular, let's look at a sample app:

{% IFrame { src: 'https://stackblitz.com/github/mgechev/code-splitting-web-dev/tree/eager?embed=1&file=src/app/app.component.ts&view=preview' } %}

Check out the implementation of the app's modules. Inside AppModule two routes are defined: the default route associated with HomeComponent and a nyan route associated with NyanComponent:

@NgModule({
  ...
  imports: [
    BrowserModule,
    RouterModule.forRoot([
      {
        path: '',
        component: HomeComponent,
        pathMatch: 'full'
      },
      {
        path: 'nyan',
        component: NyanComponent
      }
    ])
  ],
  ...
})
export class AppModule {}

Route-level code splitting

To set up code splitting, the nyan eager route needs to be refactored.

Version 8.1.0 of the Angular CLI can do everything for you with this command:

ng g module nyan --module app --route nyan

This will generate:

Let's go through these steps manually so we get a better understanding of implementing code splitting with Angular!

When the user navigates to the nyan route, the router will render the NyanComponent in the router outlet.

To use route-level code splitting in Angular, set the loadChildren property of the route declaration and combine it with a dynamic import:

{
  path: 'nyan',
  loadChildren: () => import('./nyan/nyan.module').then(m => m.NyanModule)
}

There are a two key differences from the eager route:

  1. You set loadChildren instead of component. When using route-level code splitting you need to point to dynamically loaded modules, instead of components.
  2. In loadChildren, once the promise is resolved you return the NyanModule instead of pointing to the NyanComponent.

The snippet above specifies that when the user navigates to nyan, Angular should dynamically load nyan.module from the nyan directory and render the component associated with the default route declared in the module.

You can associate the default route with a component using this declaration:

import { NgModule } from '@angular/core';
import { NyanComponent } from './nyan.component';
import { RouterModule } from '@angular/router';

@NgModule({
  declarations: [NyanComponent],
  imports: [
    RouterModule.forChild([{
      path: '',
      pathMatch: 'full',
      component: NyanComponent
    }])
  ]
})
export class NyanModule {}

This code renders NyanComponent when the user navigates to https://example.com/nyan.

To check that the Angular router downloads the nyan.module lazily in your local environment:

{% Instruction 'devtools-network', 'ol' %}

  1. Click NYAN in the sample app.
  2. Note that the nyan-nyan-module.js file appears in the network tab.

{% Img src="image/admin/wT4xLV2OkrZ2b7QaQz8L.png", alt="Lazy-loading of JavaScript bundles with route-level code splitting", width="800", height="524" %}

Find this example on GitHub.

Show a spinner

Right now, when the user clicks the NYAN button, the application doesn't indicate that it's loading JavaScript in the background. To give the user feedback while loading the script you'll probably want to add a spinner.

To do that, start by adding markup for the indicator inside the router-outlet element in app.component.html:

<router-outlet>
  <span class="loader" *ngIf="loading"></span>
</router-outlet>

Then add an AppComponent class to handle routing events. This class will set the loading flag to true when it hears the RouteConfigLoadStart event and set the flag to false when it hears the RouteConfigLoadEnd event.

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent {
  loading: boolean;
  constructor(router: Router) {
    this.loading = false;
    router.events.subscribe(
      (event: RouterEvent): void => {
        if (event instanceof NavigationStart) {
          this.loading = true;
        } else if (event instanceof NavigationEnd) {
          this.loading = false;
        }
      }
    );
  }
}

In the example below we've introduced an artificial 500 ms latency so that you can see the spinner in action.

{% IFrame { src: 'https://stackblitz.com/github/mgechev/code-splitting-web-dev/tree/lazy?embed=1&file=src/app/app.component.ts&view=preview' } %}

{% Aside 'warning' %} Code splitting can significantly improve an app's initial load time, but it comes at the cost of slowing down subsequent navigation. In the next post on route preloading you'll see how to work around this problem! {% endAside %}

Conclusion

You can shrink the bundle size of your Angular applications by applying route-level code splitting:

  1. Use the Angular CLI lazy-loaded module generator to automatically scaffold a dynamically loaded route.
  2. Add a loading indicator when the user navigates to a lazy route to show there's an ongoing action.

Route preloading strategies in Angular

Route-level code splitting can help you reduce the initial load time of an application by delaying the JavaScript associated with routes that aren't initially needed. This way, the Angular router waits until a user navigates to a given route before triggering a network request to download the associated JavaScript.

While this technique is great for initial page load, it can slow down navigation, depending on the users' network latency and bandwidth. One way to tackle this problem is route preloading. Using preloading, when the user is at a given route, you can download and cache JavaScript associated with routes that are likely to be needed next. The Angular router provides this functionality out of the box.

In this post, you'll learn how to speed up navigation when using route-level code splitting by taking advantage of JavaScript preloading in Angular.

Route preloading strategies in Angular

The Angular router provides a configuration property called preloadingStrategy, which defines the logic for preloading and processing lazy-loaded Angular modules. We'll cover two possible strategies:

The rest of this post refers to a sample Angular app. You can find the source code on GitHub.

Using the PreloadAllModules strategy

The sample app has several lazy-loaded routes. To preload all of them using the PreloadAllModules strategy—which is built into Angular—specify it as the value for the preloadingStrategy property in the router configuration:

import { RouterModule, PreloadAllModules } from '@angular/router';
// …

RouterModule.forRoot([
  …
], {
  preloadingStrategy: PreloadAllModules
})
// …

Now serve the application and look at the Network panel in Chrome DevTools:

{% Instruction 'devtools-network', 'ol' %}

You should see that the router downloaded nyan-nyan-module.js and about-about-module.js in the background when you opened the application:

{% Video src=["video/tcFciHGuF3MxnTr1y5ue01OGLBn2/TVi6LCasiwZI1hxJrBOL.webm", "video/tcFciHGuF3MxnTr1y5ue01OGLBn2/e9h6JBVl8TUGMWOSAWyC.mp4"], controls="true", loop="true", muted="true" %}
The PreloadAllModules strategy in action.

The router also registered the modules' route declarations so that when you navigate to a URL associated with any of the preloaded modules, the transition is instantaneous.

PreloadAllModules is useful in a lot of cases. When you have dozens of modules, however, its aggressive preloading can really increase network usage. Also, since the router needs to register the routes in all the preloaded modules, it can cause intensive computations in the UI thread and lead to sluggish user experience.

You can add quicklink to an Angular app by using the ngx-quicklink package. Start by installing the package from npm:

npm install --save ngx-quicklink

Once it's available in your project, you can use QuicklinkStrategy by specifying the router's preloadingStrategy and importing the QuicklinkModule:

import {QuicklinkStrategy, QuicklinkModule} from 'ngx-quicklink';
…

@NgModule({
  …
  imports: [
    …
    QuicklinkModule,
    RouterModule.forRoot([…], {
      preloadingStrategy: QuicklinkStrategy
    })
  ],
  …
})
export class AppModule {}

Now when you open the application again, you'll notice that the router only preloads nyan-nyan-module.js since the button in the center of the page has a router link to it. And when you open the side navigation, you'll notice that the router then preloads the "About" route:

{% Video src=["video/tcFciHGuF3MxnTr1y5ue01OGLBn2/dfZkoiQyNh4fUj4DJjrc.webm", "video/tcFciHGuF3MxnTr1y5ue01OGLBn2/SkNp99W1Bv2tsaRgOwoe.mp4"], controls="true", loop="true", muted="true" %}
A demo of the quicklink preloading strategy.

The above example will work for a basic application but if your application contains multiple lazy-loaded modules you will need to import the QuicklinkModule into a shared module, export it and then import the shared module into your lazy-loaded modules.

First import the QuicklinkModule from ngx-quicklink into your shared module and export it:

import { QuicklinkModule } from 'ngx-quicklink';
…

@NgModule({
  …
  imports: [
    QuicklinkModule
  ],
  exports: [
    QuicklinkModule
  ],
  …
})
export class SharedModule {}

Then import your SharedModule into all of your lazy-loaded modules:

import { SharedModule } from '@app/shared/shared.module';
…

@NgModule({
  …
  imports: [
      SharedModule
  ],
  …
});

Going beyond basic preloading

While selective preloading via quicklink can significantly speed up navigation, you can make your preloading strategy even more network efficient by using predictive preloading—which is implemented by Guess.js. By analyzing a report from Google Analytics or another analytics provider, Guess.js can predict a user's navigation journey and preload only the JavaScript chunks that are likely to be needed next.

You can learn how to use Guess.js with Angular on this page from the Guess.js site.

Conclusion

To speed up navigation when using route-level code splitting:

  1. Pick the right preloading strategy depending on the size of your application:
    • Applications with few modules can use Angular's built-in PreloadAllModules strategy.
    • Applications with many modules should use a custom preloading strategy, like Angular's quicklink, or predictive preloading, as implemented in Guess.js.
  2. Configure the preloading strategy by setting the preloadStrategy property of the Angular router.

Virtualize large lists with the Angular CDK

The scrolling list is one of the most common UI patterns today, whether it's browsing an infinitely scrolling feed on your favorite social media site, or navigating an enterprise dashboard. When scrolling lists become very long (hundreds, thousands, or hundreds of thousands of items), application performance can suffer.

Large lists can be slow to load because the application must load and render all the data up front. They can also be slow to render and navigate because each item in the list can have rich data , media, and functionality.

Users can experience problems when they load or scroll the page, leading to frustration and page abandonment.

Virtual scrolling in Angular with the Component Dev Kit

Virtual scrolling is the primary technique used to address these scale problems. Virtual scrolling gives the impression of a very large list—by providing an appropriately sized scroll bar—and the ability to navigate the list without requiring the application to hold the entire list in memory or render it on the page.

In Angular, virtual scrolling is provided by the Component Dev Kit (CDK). By modifying the way you iterate through lists, and by supplying a couple of additional configuration parameters, the CDK's virtual scrolling will automatically manage the virtual rendering of your lists, improving page performance and responsiveness.

Instead of rendering the entire list at a time, only a subset of the items that fits on the screen (plus a small buffer) will be rendered. As the user navigates, a new subset of items is calculated and rendered, re-using the existing DOM if desired.

The rest of this post walks through how to set up basic virtual scrolling. You can see a full working example in this sample app:

{% IFrame { src: 'https://stackblitz.com/edit/scroll-list?embed=1&file=src/app/app.component.ts&view=preview' } %}

Setting up virtual scrolling

First make sure you've installed @angular/cdk using your favorite package manager. To install it using npm run this command in the terminal:

npm install --save @angular/cdk

Add ScrollingModule to your app

With the CDK installed, import ScrollingModule, which handles virtual scrolling, from the @angular/cdk/scrolling package. Then add it to the imports array of your module:

import {ScrollingModule} from '@angular/cdk/scrolling';

...
imports: [
  ScrollingModule
...
]
...

Create a viewport

To see how the package works, try creating a component with a simple list of numbers from 0 to 99,999:

@Component({
  template: `<div *ngFor="let item of list">{% raw %}{{item}}{% endraw %}</div>`
})
export class ScrollComponent {
  list = Array.from({length: 100000}).map((_, i) => i);
}

When the browser renders the app, it has to render 100,000 individual <div> elements. This might be fine for simple text nodes, but any complexity in the repeated template will not scale well, and any event listeners will be multiplied significantly.

To add virtual scrolling and avoid those problems, you need to create a viewport by wrapping the list in a <cdk-virtual-scroll-viewport> element:

@Component({
  template: `<cdk-virtual-scroll-viewport>
    <div *ngFor="let item of list">{% raw %}{{item}}{% endraw %}</div>
    </cdk-virtual-scroll-viewport>`
})
export class ScrollComponent {
  list = Array.from({length: 100000}).map((_, i) => i);
}

Because ScrollingModule dynamically renders subsets of the list, you have to specify the height of the viewport via standard CSS. You also need to give the viewport a hint about its content by specifying the itemSize. The module uses this information to determine how many items to keep in the DOM at a given time and how to render an appropriately sized scrollbar.

@Component({
  template: `<cdk-virtual-scroll-viewport itemSize="18" style="height:80vh">
    <div *ngFor="let item of list">{% raw %}{{item}}{% endraw %}</div>
    </cdk-virtual-scroll-viewport>`
})
export class ScrollComponent {
  list = Array.from({length: 100000}).map((_, i) => i);
}

Finally, convert *ngFor to *cdkVirtualFor:

@Component({
  template: `<cdk-virtual-scroll-viewport itemSize="18" style="height:80vh">
    <div *cdkVirtualFor="let item of list">{% raw %}{{item}}{% endraw %}</div>
    </cdk-virtual-scroll-viewport>`
})
export class ScrollComponent {
  list = Array.from({length: 100000}).map((_, i) => i);
}

Instead of iterating through the entire list, the viewport will dynamically identify and iterate through the correct subset of the list for the user. Now when the user loads the page, the CDK should render the subset of the list that fits on the screen (plus a bit of buffer), and any scrolling events in the viewport will load and render the appropriate subset of the list:

{% Video src=["video/tcFciHGuF3MxnTr1y5ue01OGLBn2/hplr8uuGGlq7xhxfkeca.webm", "video/tcFciHGuF3MxnTr1y5ue01OGLBn2/tVvS430R28iEmDEGLewX.mp4"], autoplay=true, loop=true, muted=true, playsinline=true %}
The CDK rendering subsets of a list as the user scrolls.

Going further

The CDK's virtual scroll abilities go much further than this basic example. In the sample app, the entire list was in memory, but the list could be fetched on demand for more complex applications. You can learn more about the other capabilities of ScrollingModule and the cdkVirtualOf directive by reading about Scrolling in the CDK documentation.

Hero image by Mr Cup / Fabien Barral on Unsplash.