---
title: Angular Performance Optimization - Virtual Scroll
url: https://blog.octoperf.com/angular-performance-optimization---virtual-scroll/
date: '2025-10-10'
authors:
- gpereira
categories:
- Development
tags:
- Angular
- Application Performance
- Debug
description: Understanding and using virtual-scrolling to improve the performances of an Angular application that displays a large number of elements.
---

# Angular Performance Optimization - Virtual Scroll

Learn how virtual scrolling boosts Angular app performance when rendering very large lists.  
Classic scrolling forces the browser to render every DOM element, while virtual scrolling keeps only visible items and small buffers.  
With Angular CDK’s `cdk-virtual-scroll-viewport`, massive datasets become lightweight to display—provided each item has a predictable height.  
The guide shows how to integrate CDK Virtual Scroll, adjust components, and fix update issues linked to array references.  
Switching to a `BehaviorSubject` offers a cleaner, reactive way to propagate list changes.  
Performance metrics reveal drastic improvements in rendering and change detection times.  
Use these techniques to make large, scrollable lists smooth and highly responsive in Angular applications.


## Table of Contents
- [Introduction](#introduction) 
- [The concept of Virtual Scroll](#the-concept-of-virtual-scroll)
- [Angular CDK virtual scroll](#angular-cdk-virtual-scroll)
- [Books List implementation](#books-list-implementation)
- [Virtual Scroll Performances Check](#virtual-scroll-performances-check)
- [Conclusion](#conclusion)


## Introduction

This is the third blog post dedicated to **Angular performance optimizations**. 
In the previous one we saw [how to set the `ChangeDetectionStrategy` of a component to improve the performances](https://blog.octoperf.com/angular-performance-optimization---onpush-change-detection/) of a simple web application that displays a list of 100000 books (a simple component that shows a title and an author).

`ChangeDetectionStrategy.OnPush` strategy doubled the performances of said application, but there is still room for improvement.
Indeed, rendering this many elements can be slow in any web browser. 

So the subject of the current blog post is **how to use virtual-scrolling to improve the performances of an Angular application**?

## The concept of Virtual Scroll

### Classic scrolling

When the number of displayed element does not fit inside their container (the *viewport*), a scrollbar is usually displayed to browse through the list.
But the **invisible elements are still rendered in the DOM**:

![Normal Scrolling Schema](https://blog.octoperf.com/img/blog/angular-performance-virtual-scroll/classic-scroll-schema.png "Normal Scrolling Schema")

Here the number of rendered *Items* does not depend on the viewport size. **ALL of them are always rendered, potentially leading to a performance issue.**
This is necessary for the web browser to compute their height, and match the scrollbar size and position to the viewport content.

The consequence for our Books list use case is that even though only a small portion of the books are visible on the screen, all 100,000 are in fact rendered:

![Books list DOM](https://blog.octoperf.com/img/blog/angular-performance-virtual-scroll/dom-ul-li-no-virtual-scroll.png "Books list DOM")

### Virtual scrolling

The idea of virtual scrolling is to **only display the items that are visible in the viewport**, plus "before" and "after" buffers to avoid glitches when the user moves the scrollbar: 

![Virtual Scrolling Schema](https://blog.octoperf.com/img/blog/angular-performance-virtual-scroll/virtual-scroll-schema.png "Virtual Scrolling Schema")

The advantage is obviously the performance improvement.
**The downside is that the rendered items must have a fixed size** (or at least a predictable size without rendering them) in order to compute the fake scrollbar position and height.

## Angular CDK virtual scroll

An implementation of the virtual scroll strategy is available in the [Angular Material CDK Scrolling Module](https://material.angular.io/cdk/scrolling/overview).

> Loading hundreds of elements can be slow in any browser; **virtual scrolling enables a performant way to simulate all items being rendered** by making the height of the container element the same as the height of total number of elements to be rendered, and then only rendering the items in view.

The viewport described above is done by the `<cdk-virtual-scroll-viewport>` component.
Its `[itemSize]` input must be set to give the item height in pixels (unless you use a [custom scroll strategy](https://material.angular.io/cdk/scrolling/examples#cdk-virtual-scroll-custom-strategy)).

Instead of using the `*ngFor` directive to iterate over the items, you must use the dedicated `*cdkVirtualFor` directive that supports the exact same API.

### CdkVirtualScroll Installation

The Angular CDK library can be added by updating your project's `package.json` file (set the version to match your `@angular/core` version):

```json
{
  "dependencies": {
    "@angular/cdk": "^14.2.0"
  }
}
```

Then install it by typing the command `npm install` in a terminal at the root of your Angular project.

## Books List implementation

Back to our Books list application. See the previous blog post about [Angular performance improvement using ChangeDetectionPolicy](https://blog.octoperf.com/angular-performance-optimization---onpush-change-detection/#use-case-books-list) to know more about our simple use case.

### AppModule import

First, the module `ScrollingModule` must be added to the imports of our root *AppModule*:

```typescript
import {NgModule} from '@angular/core';
import {BrowserModule} from '@angular/platform-browser';

import {AppComponent} from './app.component';
import {ReactiveFormsModule} from "@angular/forms";
import {BookComponent} from './book/book.component';
import {ScrollingModule} from "@angular/cdk/scrolling";

@NgModule({
    declarations: [
        AppComponent,
        BookComponent,
    ],
    imports: [
        BrowserModule,
        ReactiveFormsModule,
        ScrollingModule
    ],
    bootstrap: [AppComponent]
})
export class AppModule {
}
```

### AppComponent update

The previous version of the `AppComponent` component was using a simple `*ngFor` directive and `<ul><li>` for the layout:

```html
...
<ul>
  <li *ngFor="let book of books">
    <app-book [book]="book"></app-book>
  </li>
</ul>
```

We must rewrite its HTML template to use the CDK virtual scroll:

```html
<form [formGroup]="formGroup">
  <input type="number" [formControl]="indexControl">
  <input type="text" [formControl]="titleControl">
  <button (click)="update()">Update</button>
</form>

<cdk-virtual-scroll-viewport itemSize="20" class="viewport">
  <app-book-on-push *cdkVirtualFor="let book of books" 
                    [book]="book" class="item">
  </app-book-on-push>
</cdk-virtual-scroll-viewport>
```

The `itemSize` is set to 20 pixels. 
Update the `app.component.scss` file to have a matching fixed *20px* height for the items and *200px* for the viewport (10 books should be visible at once):

```scss
.viewport {
  height: 200px;
}

.item {
  display: block;
  height: 20px;
}
```

Start the application with `ng serve` and open it.
Unfortunately, clicking on the *Update* button does not seem to update the book title:

![Books list bug](https://blog.octoperf.com/img/blog/angular-performance-virtual-scroll/books-list-no-changes.png "Books list bug")

### Books array reference bug

If you play with the scrollbar, hide the updated book and show it again, you may see that its title is now updated.
It feels like **the CDK Virtual Scroll is not aware that a modification was made** to the books list, and only refreshes its content when the scrollbar is used. 

The current implementation of the `update()` method is: 

```typescript
export class AppComponent {

    books: Book[] = [...Array(100000).keys()]
      .map((index) => new Book(index, `Title ${index}`, `Author ${index}`));  

    update(): void {
        const value = this.formGroup.getRawValue();
        const book = this.books[value.index];
        if (book) {
            book.title = value.title;
            const updated = new Book(book.id, value.title, book.author);
            this.books[value.index] = updated;
        }
    }
}
```

We force an update of the book reference with the line `this.books[value.index] = updated;`. That triggers the change detection of the `BookComponent`.

**But the *AppComponent* `books` array reference is not updated by this operation.**
A quick and dirty fix is to reset this array reference with `this.books = this.books.concat();` :

```typescript
export class AppComponent {

    books: Book[] = [...Array(100000).keys()]
      .map((index) => new Book(index, `Title ${index}`, `Author ${index}`));

    update(): void {
        const value = this.formGroup.getRawValue();
        const book = this.books[value.index];
        if (book) {
            book.title = value.title;
            const updated = new Book(book.id, value.title, book.author);
            this.books[value.index] = updated;
            this.books = this.books.concat();
        }
    }
}
```

Back to the application, we can see now that clicking on the *Update* button immediately sets a new title in the books list:

![Books list fix](https://blog.octoperf.com/img/blog/angular-performance-virtual-scroll/books-list-updated-title.png "Books list fix")

But there might be a better way to code this?

### Behavior Subject

**A cleaner solution is to use a `BehaviorSubject` instead** of a raw array for our books list:

```typescript
export class AppComponent {
    books: BehaviorSubject<Book[]> = new BehaviorSubject<Book[]>(
      [...Array(100000).keys()]
        .map((index) => new Book(index, `Title ${index}`, `Author ${index}`))
    );

    update(): void {
        const value = this.formGroup.getRawValue();
        const books = this.books.value;
        const book = books[value.index];
        if (book) {
            book.title = value.title;
            const updated = new Book(book.id, value.title, book.author);
            books[value.index] = updated;
            this.books.next(books);
        }
    }
}
```
No change is needed in the HTML template as the `*cdkVirtualFor` directive can take an observable input:

```html
  <app-book-on-push *cdkVirtualFor="let book of books" [book]="book" class="item"></app-book-on-push>
```

On the plus side, using a *BehaviorSubject* allows you to easily react to changes made to the books list in your application: all *Subjects* extend [RxJs Observables](https://rxjs.dev/guide/observable). 

Now that the code is properly fixed, let's open the web browser console to inspect the DOM:

![DOM with virtual scrolling](https://blog.octoperf.com/img/blog/angular-performance-virtual-scroll/dom-with-virtual-scroll.png)

We can see that only a few `<app-book>` elements are rendered: the 10 visible ones plus a few others for the buffer.
**How does that translate in terms of performances?**

## Virtual Scroll Performances Check

> Check our previous blog post to [install the tools required to measure Angular change detection cycle performances](https://blog.octoperf.com/angular-performance-optimization---onpush-change-detection/#measure-the-change-detection-cycle-time).

**The application performances are now perfect!** The `timeChangeDetection()` debug method gives us 0.01ms per check:

![ng.profiler.timeChangeDetection()](https://blog.octoperf.com/img/blog/angular-performance-virtual-scroll/time-change-detection.png "ng.profiler.timeChangeDetection()")

The profiler gives us 3.4ms to handle the Book update:

![Angular Devtools Profiler](https://blog.octoperf.com/img/blog/angular-performance-virtual-scroll/angular-profiler-result.png "Angular Devtools Profiler")

## Conclusion

We studied the concept of virtual scrolling and used it in our Books list sample application with the Angular CDK Scrolling module. 

For a large list of elements, **the performance improvements induced by virtual scrolling is impressive!**

But implementing it is not always that simple.
We managed to [use virtual scroll for a tree](https://blog.octoperf.com/angular-how-to-use-virtual-scroll-with-a-flat-tree/) in OctoPerf's [load testing application](https://octoperf.com).
We also wrapped [Angular Material table](https://material.angular.io/components/table/overview): dynamically sized sticky headers and footers are used as placeholders for invisible rows.

An issue is opened on [Angular's GitHub](https://github.com/angular/components/issues/10122) to integrate virtual-scrolling as an add-on for existing components.
