How to use virtual scrolling to load images in Angular

Luis Aviles

Web Performance is something that is becoming more important every day for users and developers. When the user enters a website, they can get impatient if it does not load within a few seconds and eventually leave the site.

Let's say you're developing a web application that involves the rendering of hundreds or even thousands of elements.

In that kind of scenario, you may have to process a large set of data and the performance can be drastically affected due to the number of nodes that are being created in the DOM (the more data to display, the bigger the DOM tree).

In this article, I'll explain a useful strategy to reduce the amount of rendered DOM nodes regardless of the size of your data set: Virtual Scrolling

The Problem

Let's suppose you're building a web application that needs to render an image gallery of thousands of users. The app retrieves the images from a large set of data.

However, it's required to display them within a defined area as the following screenshot shows.

Given that scenario, it's logical to think that it does not make much sense to render all the elements at the same time, but only the necessary ones.

Virtual Scrolling

Most popular frameworks have an implementation for virtualized content. Also, it's worth mentioning the infinite list study group that covers research over virtualization solutions for the web and other platforms.

In a context of a Virtual Scroll implementation, the items will be removed or added as follows:

  • It will delete the invisible rows while the user is going down through the scroll
  • It will add new rows at runtime
  • The View Port is the area that renders only the necessary elements

The Angular CDK Solution

The Angular CDK is a set of behavior primitives for building UI components. Also, the Angular Material components have been defined using these utilities.

You can find more resources to learn about Angular CDK(and scrolling) at the end of this article.

Implementation

The Source of Data

For this project, we'll use the Random User Generator API, which is free and allows us to generate random user data.

Just open a new tab in your favorite browser to see an example: https://randomuser.me/api/. The result will come as a User object with several properties.

Now try the following URL to get the pictures only of 100 users: https://randomuser.me/api/?results=100&inc=picture. You'll get a result as follows:

1{
2 "results": [
3 {
4 "picture": {
5 "large": "https://randomuser.me/api/portraits/women/96.jpg",
6 "medium": "https://randomuser.me/api/portraits/med/women/96.jpg",
7 "thumbnail": "https://randomuser.me/api/portraits/thumb/women/96.jpg"
8 }
9 },
10 {
11 "picture": {
12 "large": "https://randomuser.me/api/portraits/women/77.jpg",
13 "medium": "https://randomuser.me/api/portraits/med/women/77.jpg",
14 "thumbnail": "https://randomuser.me/api/portraits/thumb/women/77.jpg"
15 }
16 }
17 ]
18}

We're ready to go! Let's create the project as a next step.

The Initial Project

Let's create a brand new Angular project using the Angular CLI tool.

1ng new load-images-virtual-scroll --routing --style css --prefix demo

Next, let's add the Angular Material dependency, which will install the Angular CDK library under the hood.

1ng add @angular/material

Then, let's create a couple of modules and components to have a good code architecture.

1ng generate module shared --module app
2ng generate module shared/material --module shared
3ng generate module gallery --module app
4ng generate component gallery --module app
5ng generate component gallery/gallery
6ng generate service gallery/gallery

Pay attention to every output of those commands to understand what's happening with your project and directories. We'll have the following structure:

1|- src/
2 |- app/
3 |- app.module.ts
4 |- app.component.ts|html|css
5 |- gallery/
6 |- gallery.module.ts
7 |- gallery.component.ts|html|css
8 |- gallery.service.ts
9 |- shared/
10 |- shared.module.ts
11 |- material/
12 |- material.module.ts

The Data Model

It's time to define the data model. Create a new file app/gallery/user.ts

1export interface User {
2 picture: {
3 large: string;
4 medium: string;
5 };
6}

Using Angular Material Components

Before start using the Angular Material components, let's add their modules in the material.module.ts file.

1// material.module.ts
2// ... other imports
3import { MatToolbarModule } from '@angular/material/toolbar';
4import { MatGridListModule } from '@angular/material/grid-list';
5import { ScrollingModule, CdkScrollableModule} from '@angular/cdk/scrolling';
6
7@NgModule({
8 declarations: [],
9 imports: [
10 CommonModule,
11 MatToolbarModule,
12 MatGridListModule,
13 ScrollingModule,
14 CdkScrollableModule
15 ],
16 exports: [
17 MatToolbarModule,
18 MatGridListModule,
19 ScrollingModule,
20 CdkScrollableModule
21 ]
22})
23export class MaterialModule { }

This module is intended to define all material and CDK modules. That's why we added the ScrollingModule and CdkScrollableModule from the @angular/cdk/scrolling package. This is important before start using the Virtual Scrolling implementation.

Next, update the shared.module.ts file:

1// shared.module.ts
2
3// ...other imports
4import { MaterialModule } from './material/material.module';
5
6
7@NgModule({
8 declarations: [],
9 imports: [
10 CommonModule,
11 MaterialModule
12 ],
13 exports: [
14 MaterialModule
15 ]
16})
17export class SharedModule { }

Creating the Angular Service

Update the content of the gallery.service.ts file:

1import { HttpClient } from '@angular/common/http';
2import { Injectable } from '@angular/core';
3import { Observable } from 'rxjs';
4import { User } from './user';
5
6
7@Injectable({
8 providedIn: 'root'
9})
10export class GalleryService {
11
12 constructor(private httpClient: HttpClient) { }
13
14 getImages(count: number = 100): Observable<{results: User[]}> {
15 return this.httpClient.get<{results: User[]}>(`https://randomuser.me/api/?results=${count}&inc=picture`);
16 }
17}

This Angular Service defines a single method getImages to retrieve a list of objects containing the required images for the gallery.

Creating the Gallery Component

Let's update the gallery.component.ts file with the following content.

1// gallery.component.ts
2
3import { Component, OnInit } from '@angular/core';
4import { Observable } from 'rxjs';
5import { GalleryService } from '../gallery.service';
6import { User } from '../user';
7import { map } from 'rxjs/operators';
8
9@Component({
10 // ...
11})
12export class GalleryComponent implements OnInit {
13 users$: Observable<User[]>;
14
15 constructor(private galleryService: GalleryService) {}
16
17 ngOnInit(): void {
18 this.users$ = this.galleryService
19 .getImages(120)
20 .pipe(map(({ results }) => results));
21 }
22}

So what's happening here?

  • It creates an Observable attribute users$ to process the data as a stream.
  • It injects a GalleryService instance using the DI framework(Dependency Injection)
  • Once the component is being initialized(ngOnInit) it will perform an HTTP call through the service.
  • Since the app is interested in the images only, we can use the .pipe() function to use a map operator to "extract" the results attribute only(See the example result from the API above).

We already got the data at this point. The next step involves the creation of the View Port to start using the Virtual Scrolling solution.

Add the following code in the gallery.component.html file.

1<cdk-virtual-scroll-viewport itemSize="42" class="viewport">
2 <mat-grid-list cols="4" rowHeight="1:1">
3 <mat-grid-tile *cdkVirtualFor="let user of users$ | async">
4 <img [src]="user.picture.large" />
5 </mat-grid-tile>
6 </mat-grid-list>
7</cdk-virtual-scroll-viewport>

Let's explain the previous code snippet.

  • The <cdk-virtual-scroll-viewport> element defines the View Port area to be able to render the items that fit in it.
  • The itemSize directive tells the cdk-virtual-scroll-viewport the size of the items in the list(in pixels).
  • The class="viewport" is important here since, by default, the size of the View Port is 0, which means you'll see a blank page only if the dimension is not set.
  • The *cdkVirtualFor is required here instead of using *ngFor(from Angular). This directive has the same API as *ngFor.
  • The <mat-grid-list> defines the layout for the gallery and the<mat-grid-tile> element will contain every image. You can find more details about the Grid list component in the official documentation.

Important! Do not forget to edit the gallery.component.css file to set the View Port size(width x height).

1mat-grid-tile {
2 background: lightblue;
3}
4
5.viewport {
6 height: 500px;
7 max-width: 700px;
8 margin-left: auto;
9 margin-right: auto;
10 border: 1px solid #e9e7e9;
11}

As a final step, you can open your browser's developer tools to inspect the DOM and verify that elements are dynamically being added and removed as the next screenshot shows.

Live Demo

This project is available on GitHub and CodeSandbox.

If you prefer, you can play around with the project here too:

Resources

Feel free to reach out on Twitter if you have any questions. Follow me on GitHub to see more about my work.

Luis Aviles

Senior Software Engineer

Luis is a Senior Software Engineer and Google Developer Expert in Web Technologies and Angular. He is an author of online courses, technical articles, and a public speaker. He has participated in different international technology conferences, giving technical talks, workshops, and training sessions. He’s passionate about the developer community and he loves to help junior developers and professionals to improve their skills.
When he’s not coding, Luis is doing photography or Astrophotography.