How to Handle Large Datasets in Angular Without Compromising Performance
Let me walk you through my approach to handling large datasets efficiently in Angular applications. The key is to use a combination of techniques:
- Virtual Scrolling
- Data Pagination
- Lazy Loading
- Efficient State Management
- Web Workers for Heavy Computations
Here’s how I implement these strategies:
// 1. Virtual Scrolling Implementation
@Component({
selector: 'app-virtual-list',
template: `
<cdk-virtual-scroll-viewport
[itemSize]="50"
class="viewport"
>
@for (item of items(); track trackById) {
<div
class="item"
[style.height.px]="50"
>
<item-card [data]="item" />
</div>
}
</cdk-virtual-scroll-viewport>
`,
styles: [`
.viewport {
height: 500px;
width: 100%;
overflow-y: auto;
}
.item {
display: flex;
align-items: center;
}
`],
standalone: true,
imports: [ScrollingModule]
})
export class VirtualListComponent {
private dataService = inject(DataService);
// Use signals for reactive updates
items = signal<ListItem[]>([]);
loading = signal(false);
// Track items by ID for efficiency
trackById(index: number, item: ListItem) {
return item.id;
}
// Load data in chunks
loadChunk(startIndex: number, count: number) {
this.loading.set(true);
return this.dataService.getItems(
startIndex,
count
).pipe(
tap(newItems => {
this.items.update(items => [
...items,
...newItems
]);
}),
finalize(() => this.loading.set(false))
);
}
}
// 2. Efficient Data Service
@Component({
selector: 'app-data-grid',
template: `
<div class="grid-container">
<!-- Toolbar with filters -->
<div class="toolbar">
<input
[formControl]="searchControl"
placeholder="Search..."
/>
<select [formControl]="sortControl">
<option value="name">Name</option>
<option value="date">Date</option>
</select>
</div>
<!-- Virtual scrolling grid -->
<cdk-virtual-scroll-viewport
[itemSize]="50"
class="viewport"
>
@for (row of filteredData(); track trackById) {
<div class="grid-row">
@for (col of columns; track col) {
<div class="grid-cell">
{{ row[col] }}
</div>
}
</div>
}
</cdk-virtual-scroll-viewport>
<!-- Loading indicator -->
@if (loading()) {
<div class="loader">Loading...</div>
}
<!-- Pagination -->
<app-paginator
[total]="totalItems()"
[pageSize]="pageSize()"
[currentPage]="currentPage()"
(pageChange)="onPageChange($event)"
/>
</div>
`
})
export class DataGridComponent {
private worker = inject(WorkerService);
private destroy$ = inject(DestroyRef);
// Form controls
searchControl = new FormControl('');
sortControl = new FormControl('name');
// Pagination state
currentPage = signal(0);
pageSize = signal(50);
totalItems = signal(0);
// Data state
rawData = signal<any[]>([]);
loading = signal(false);
// Computed filtered data
filteredData = computed(() => {
const data = this.rawData();
const search = this.searchControl.value?.toLowerCase();
const sort = this.sortControl.value;
// Use worker for heavy computations
return this.worker.filterAndSort(
data,
search,
sort
);
});
constructor() {
// Handle search and sort changes
merge(
this.searchControl.valueChanges,
this.sortControl.valueChanges
).pipe(
debounceTime(300),
takeUntilDestroyed(this.destroy$)
).subscribe(() => {
this.currentPage.set(0);
this.loadData();
});
}
loadData() {
this.loading.set(true);
const params = {
page: this.currentPage(),
pageSize: this.pageSize(),
search: this.searchControl.value,
sort: this.sortControl.value
};
this.dataService.getData(params).pipe(
tap(response => {
this.rawData.set(response.data);
this.totalItems.set(response.total);
}),
finalize(() => this.loading.set(false))
).subscribe();
}
}
// 3. Web Worker for Heavy Computations
// data.worker.ts
self.addEventListener('message', ({ data }) => {
const { items, search, sort } = data;
// Filter data
let filtered = search ?
items.filter(item =>
item.name.toLowerCase().includes(search)
) : items;
// Sort data
filtered.sort((a, b) => {
const aVal = a[sort];
const bVal = b[sort];
return aVal < bVal ? -1 : 1;
});
self.postMessage({ filtered });
});
// 4. Worker Service
@Injectable({ providedIn: 'root' })
export class WorkerService {
private worker: Worker;
constructor() {
this.worker = new Worker(
new URL('./data.worker', import.meta.url)
);
}
filterAndSort(
items: any[],
search: string,
sort: string
): Observable<any[]> {
return new Observable(observer => {
this.worker.postMessage({ items, search, sort });
const handler = ({ data }: MessageEvent) => {
observer.next(data.filtered);
observer.complete();
};
this.worker.addEventListener('message', handler);
return () => {
this.worker.removeEventListener('message', handler);
};
});
}
}
Real-world optimization example:
// Before optimization
@Component({
template: `
<div *ngFor="let item of items">
{{ computeValue(item) }}
</div>
`
})
// After optimization
@Component({
template: `
<cdk-virtual-scroll-viewport [itemSize]="50">
@for (item of visibleItems(); track trackById) {
{{ cachedValues()[item.id] }}
}
</cdk-virtual-scroll-viewport>
`,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class OptimizedComponent {
private cache = new Map<string, any>();
// Computed cache
cachedValues = computed(() => {
const values = {};
for (const item of this.visibleItems()) {
if (!this.cache.has(item.id)) {
this.cache.set(
item.id,
this.computeValue(item)
);
}
values[item.id] = this.cache.get(item.id);
}
return values;
});
}
Key points I emphasize in interviews:
Virtual Scrolling
- Essential for large lists
- Maintains performance
- Reduces memory usage
Data Management
- Efficient pagination
- Caching strategies
- Background processing
Performance Optimization
- OnPush change detection
- Computed values
- Proper tracking
Best Practices
- Use proper data structures
- Implement caching
- Optimize computations
- Monitor performance
This approach has helped me handle datasets with millions of records while maintaining smooth performance and good user experience.