← Back to Articles         
How do you optimize large lists?

How do you optimize large lists?

Published on 02 Oct 2026     10 min read Angular
Performance

How Do You Optimize Large Lists in Angular?

What is Large List Optimization?

Large list optimization means reducing the amount of DOM rendering, change detection, memory usage, and data transfer when displaying hundreds or thousands of records.

For example, suppose the API contains:

100,000 Products

A poor approach is:

SQL Server
    ↓
Return 100,000 records
    ↓
ASP.NET Core API
    ↓
Angular receives 100,000 records
    ↓
@for renders 100,000 rows
    ↓
Huge DOM + memory + rendering cost

A better approach is:

SQL Server
    ↓
Server-side pagination/filtering
    ↓
Return only 20–50 records
    ↓
Angular
    ↓
Render only those records

The most important techniques are:

  1. Server-side pagination
  2. Virtual scrolling
  3. Stable track keys
  4. OnPush change detection
  5. Signals / efficient state management
  6. Filtering and sorting on the server for very large datasets
  7. Avoid expensive template calculations

Full Simple Sample Program

Let's assume the backend already returns one page at a time.

API:

GET /api/products?page=1&pageSize=20

Response:

{
  "items": [
    {
      "id": 1,
      "name": "Laptop",
      "price": 50000
    }
  ],
  "totalCount": 10000
}

product.ts

export interface Product {
  id: number;
  name: string;
  price: number;
}

export interface ProductResponse {
  items: Product[];
  totalCount: number;
}

product.service.ts

import { inject, Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';

import { ProductResponse } from './product';

@Injectable({
  providedIn: 'root'
})
export class ProductService {

  private http = inject(HttpClient);

  getProducts(
    page: number,
    pageSize: number
  ): Observable<ProductResponse> {

    return this.http.get<ProductResponse>(
      `/api/products?page=${page}&pageSize=${pageSize}`
    );
  }
}

products.ts

import {
  ChangeDetectionStrategy,
  Component,
  inject,
  signal
} from '@angular/core';

import { ProductService } from './product.service';
import { Product } from './product';

@Component({
  selector: 'app-products',
  standalone: true,
  templateUrl: './products.html',
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class Products {

  private productService = inject(ProductService);

  products = signal<Product[]>([]);

  page = signal(1);

  pageSize = 20;

  totalCount = signal(0);

  constructor() {
    this.loadProducts();
  }

  loadProducts(): void {

    this.productService
      .getProducts(this.page(), this.pageSize)
      .subscribe(response => {

        this.products.set(response.items);

        this.totalCount.set(response.totalCount);
      });
  }

  nextPage(): void {

    if (this.page() * this.pageSize >= this.totalCount()) {
      return;
    }

    this.page.update(value => value + 1);

    this.loadProducts();
  }

  previousPage(): void {

    if (this.page() === 1) {
      return;
    }

    this.page.update(value => value - 1);

    this.loadProducts();
  }
}

products.html

<h2>Products</h2>

@for (product of products(); track product.id) {

  <div>
    {{ product.id }} -
    {{ product.name }} -
    ₹{{ product.price }}
  </div>

}

<hr>

<button
  (click)="previousPage()"
  [disabled]="page() === 1">

  Previous

</button>

<span>
  Page {{ page() }}
</span>

<button
  (click)="nextPage()"
  [disabled]="page() * pageSize >= totalCount()">

  Next

</button>

<p>
  Total Records: {{ totalCount() }}
</p>

How Does This Optimize the List?

Suppose the database contains:

100,000 products

We configure:

pageSize = 20;

Angular receives only:

20 products

at a time.

Therefore:

100,000 DB rows
       ↓
Server pagination
       ↓
20 JSON records
       ↓
Angular
       ↓
20 @for items
       ↓
~20 rendered rows

This reduces work across the entire stack.


1. Use Server-Side Pagination

For large database-backed collections, this is usually the most important optimization.

Instead of:

GET /api/products

returning:

100,000 records

request:

GET /api/products?page=1&pageSize=20

Then:

Page 1 → records 1–20
Page 2 → records 21–40
Page 3 → records 41–60

The backend can perform pagination in SQL.

For example:

SELECT
    Id,
    Name,
    Price
FROM Products
ORDER BY Id
OFFSET 0 ROWS
FETCH NEXT 20 ROWS ONLY;

This improves more than Angular rendering.

It reduces:

Database work/data transfer
        ↓
API response size
        ↓
Network traffic
        ↓
Angular memory
        ↓
DOM size

2. Use track with @for

Always provide stable identity for dynamic entity lists.

@for (product of products(); track product.id) {

  <div>
    {{ product.name }}
  </div>

}

Prefer:

track product.id

rather than:

track $index

for lists that can be added, removed, sorted, or reordered.

Suppose:

Before

101 Laptop
102 Mouse
103 Keyboard

becomes:

101 Laptop
103 Keyboard
104 Monitor

Angular can determine:

101 → retain/reuse
102 → remove
103 → retain/reuse
104 → create

This reduces unnecessary list DOM/view work.


3. Use Virtual Scrolling

Sometimes you want the user to scroll through a large collection rather than use traditional pagination.

Instead of rendering:

10,000 records

virtual scrolling might render only approximately the items visible in the viewport plus a buffer.

Conceptually:

10,000 records available

        ↓

┌────────────────────┐
│ Product 500        │
│ Product 501        │
│ Product 502        │
│ Product 503        │ ← Visible viewport
│ Product 504        │
│ Product 505        │
└────────────────────┘

Only a small window
needs DOM elements.

As the user scrolls:

Old visible items
       ↓
leave viewport

New items
       ↓
enter viewport

DOM/view window updated

Angular CDK provides virtual scrolling capabilities for this type of UI.


4. Pagination vs Virtual Scrolling

These solve related but different problems.

Pagination Virtual Scrolling
Shows one page at a time Gives continuous scrolling
Excellent for server/database data Excellent for long scrolling UIs
Very simple memory model Keeps rendered DOM small
Good for tables/search results Good for feeds/catalogs
Easy page navigation Better continuous-scroll UX

For extremely large database datasets, you may combine server-side data loading with a virtualized UI rather than downloading everything first.


5. Use OnPush

For list components:

@Component({
  changeDetection: ChangeDetectionStrategy.OnPush
})

This allows Angular to skip component subtrees when they don't need checking.

For example:

Products
│
├── Product 1
├── Product 2
├── Product 3
├── ...
└── Product 1000

If every row is a component, unnecessary checking can become expensive.

OnPush can help reduce that work when the application follows suitable state patterns.


6. Use Signals

Signals are useful for managing list state:

products = signal<Product[]>([]);

Update:

this.products.set(response.items);

For derived data:

filteredProducts = computed(() =>
  this.products().filter(...)
);

Signals provide explicit reactive dependencies and work naturally with OnPush.


7. Don't Perform Expensive Functions in Every Row

Avoid:

@for (product of products(); track product.id) {

  <div>
    {{ calculateDiscount(product) }}
  </div>

}

Imagine:

1,000 products
       ×
calculateDiscount()
       ×
repeated view checks

This can become expensive.

Where appropriate, calculate the value when data changes, use a pure pipe, or model it as derived reactive state.


8. Perform Large Searches on the Server

Suppose you have:

1,000,000 products

Don't download them all and then do:

products.filter(...)

in Angular.

Instead:

GET /api/products?search=laptop&page=1&pageSize=20

Flow:

User searches "laptop"
        ↓
Angular
        ↓
ASP.NET Core API
        ↓
SQL Server filters
        ↓
20 matching records
        ↓
Angular renders 20

For large database-backed applications, filtering belongs primarily on the server/database side.


9. Perform Large Sorting on the Server

Same principle.

Instead of:

products.sort(...)

after downloading 100,000 records, send:

GET /api/products?sortBy=price&sortOrder=asc&page=1&pageSize=20

Then:

Angular
   ↓
sortBy=price
sortOrder=asc
   ↓
API
   ↓
SQL ORDER BY
   ↓
Return required page

This is particularly relevant to the kind of search/sort/pagination screen used in typical ASP.NET Core + Angular applications.


10. Debounce Search

Suppose the user types:

L
La
Lap
Lapt
Lapto
Laptop

Without debouncing:

L      → API
La     → API
Lap    → API
Lapt   → API
Lapto  → API
Laptop → API

That's six requests.

Use:

debounceTime(300)
distinctUntilChanged()
switchMap(...)

Conceptually:

User typing...
      ↓
wait 300 ms
      ↓
"Laptop"
      ↓
one useful API request

This is especially important for large-list search screens.


11. Don't Load Unnecessary Columns

Suppose the product entity contains:

Id
Name
Price
Description
Image
CreatedDate
ModifiedDate
CreatedBy
ModifiedBy
Supplier
Category
AuditData
...

But the list page needs only:

Id
Name
Price

The API should return a lightweight list DTO.

For example:

public class ProductListDto
{
    public int Id { get; set; }

    public string Name { get; set; } = string.Empty;

    public decimal Price { get; set; }
}

This reduces:

SQL data
   ↓
API serialization
   ↓
Network payload
   ↓
Angular memory

12. Avoid Huge DOM Trees

Even if Angular itself is efficient, the browser still has to manage the DOM.

This:

20 rows

is cheap.

This:

100,000 rows

can be expensive for:

  • DOM memory
  • Layout
  • Style calculation
  • Painting
  • Event handling
  • Angular views

So:

The best optimization for a huge list is often not to render the huge list at all.

Render only what the user needs.


Recommended Architecture

For a large Angular + ASP.NET Core + SQL Server list:

                User
                  ↓
        Search / Sort / Page
                  ↓
               Angular
                  ↓
        debounce if searching
                  ↓
          ASP.NET Core API
                  ↓
       IQueryable / SQL Query
                  ↓
       WHERE      → filtering
       ORDER BY   → sorting
       OFFSET     → pagination
       FETCH
                  ↓
          20–50 records
                  ↓
               Angular
                  ↓
             Signal state
                  ↓
      @for (...; track item.id)
                  ↓
          Small DOM list

This is usually much better than:

SQL
 ↓
100,000 rows
 ↓
API
 ↓
100,000 JSON objects
 ↓
Angular
 ↓
filter/sort
 ↓
100,000 DOM rows

Key Points

  1. Don't render thousands of DOM elements unnecessarily.
  2. Use server-side pagination for large database-backed lists.
  3. Use server-side filtering and sorting for very large datasets.
  4. Use virtual scrolling for long continuous-scroll interfaces.
  5. Always use a stable track key:
track product.id
  1. Consider OnPush for list/row component trees.
  2. Signals provide efficient reactive list state.
  3. Avoid expensive functions/getters in each list row.
  4. Debounce search requests.
  5. Return only fields required by the list.
  6. Optimize both data transfer and rendering.
  7. For huge datasets, the most important principle is:

Don't load or render data the user cannot currently see or use.

Interview Questions and Answers

1. How do you optimize large lists in Angular?

Use server-side pagination/filtering, virtual scrolling where appropriate, stable track keys, OnPush, Signals, debounced search, lightweight API DTOs, and avoid expensive template calculations.

2. Why is track important?

It gives Angular stable identity for list items:

@for (product of products(); track product.id)

Angular can then efficiently retain, add, remove, and move rendered items.

3. What is virtual scrolling?

Virtual scrolling renders only a small visible portion of a large collection instead of creating DOM elements for the entire list.

4. Pagination vs virtual scrolling?

Pagination retrieves/displays data page-by-page.

Virtual scrolling provides continuous scrolling while keeping the rendered DOM window small.

For huge server datasets, virtual scrolling can also be combined with incremental server-side loading.

5. Should filtering be done in Angular or the API?

For small already-loaded collections, client-side filtering is fine.

For very large database-backed datasets, filtering should generally happen in the API/database so unnecessary records aren't transferred to the browser.

6. How do you optimize a search box for a large list?

Use:

debounceTime(300)
distinctUntilChanged()
switchMap(...)

This reduces unnecessary API requests and prevents older searches from overtaking newer ones.

7. Why use OnPush for large lists?

OnPush allows Angular to skip component subtrees that don't require checking, which can reduce change-detection work when many row components exist.

8. Is track product.id better than track $index?

For dynamic database/API lists, usually yes. A product ID remains stable when items are inserted, removed, or reordered, while an index can change.

9. Should Angular load 100,000 records and paginate them locally?

Usually no for a database-backed application.

Prefer:

Angular
   ↓
page=1&pageSize=20
   ↓
API
   ↓
Database
   ↓
Return 20 records

10. What is the best interview answer?

For large Angular lists, I avoid loading and rendering the entire dataset. I use server-side pagination, filtering and sorting, virtual scrolling when continuous scrolling is required, stable track IDs with @for, OnPush and Signals where appropriate, debounced searches, and lightweight API DTOs. The goal is to minimize network payload, Angular change-detection work, and DOM size.

← Back to Articles