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:
- Server-side pagination
- Virtual scrolling
- Stable
trackkeys OnPushchange detection- Signals / efficient state management
- Filtering and sorting on the server for very large datasets
- 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
- Don't render thousands of DOM elements unnecessarily.
- Use server-side pagination for large database-backed lists.
- Use server-side filtering and sorting for very large datasets.
- Use virtual scrolling for long continuous-scroll interfaces.
- Always use a stable
trackkey:
track product.id
- Consider
OnPushfor list/row component trees. - Signals provide efficient reactive list state.
- Avoid expensive functions/getters in each list row.
- Debounce search requests.
- Return only fields required by the list.
- Optimize both data transfer and rendering.
- 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
trackIDs with@for,OnPushand Signals where appropriate, debounced searches, and lightweight API DTOs. The goal is to minimize network payload, Angular change-detection work, and DOM size.