A dor real
A tela congela quando o usuário digita rápido numa tabela com 5 000 células. O Zone.js dispara change detection a cada tecla, mouse ou timer… e o Angular acaba percorrendo todo o componente a cada evento. Resultado: o Chrome reclama de “processando” e o cliente já está pedindo pra fechar a aba.
O básico que não resolve tudo
OnPush + trackBy continuam obrigatórios, mas o grande erro é usar two‑way binding em milhares de inputs. Troque por one‑way binding e atualize o modelo manualmente:
import { ChangeDetectionStrategy, Component } from '@angular/core';
@Component({
selector: 'app-fast-grid',
changeDetection: ChangeDetectionStrategy.OnPush,
templateUrl: './fast-grid.component.html'
})
export class FastGridComponent {
rows = Array.from({ length: 5000 }, (_, i) => ({
id: i,
value: ''
}));
trackById(_: number, item: any): number {
return item.id;
}
onCellInput(row: any, newVal: string): void {
row.value = newVal; // aqui temos uma atualização controlada
}
}
<tr *ngFor="let row of rows; trackBy: trackById">
<td>
<input
[value]="row.value"
(input)="onCellInput(row, $event.target.value)">
</td>
</tr>
- O input recebe o valor apenas uma vez (one‑way).
- O evento (input) dispara a atualização explícita, evitando que o Angular faça “dirty checking” em todos os campos a cada ciclo.
Mesmo assim, ainda há um nó de DOM para cada linha. Quando o número de linhas cresce, o custo de criar/destruir esses nós começa a pesar.
O Elefante na sala - Virtual Scroll
Antes de partir pro DOM puro, experimente o @angular/cdk/scrolling. Ele renderiza só as linhas que cabem na viewport (geralmente ~30) e recicla os elementos à medida que o usuário rola. É a primeira linha de defesa contra o “milhão de nós”.
import { CdkVirtualScrollViewport } from '@angular/cdk/scrolling';
import { ChangeDetectionStrategy, Component } from '@angular/core';
@Component({
selector: 'app-virtual-grid',
changeDetection: ChangeDetectionStrategy.OnPush,
templateUrl: './virtual-grid.component.html',
styleUrls: ['./virtual-grid.component.css']
})
export class VirtualGridComponent {
rows = Array.from({ length: 5000 }, (_, i) => ({
id: i,
value: ''
}));
trackById(_: number, item: any): number {
return item.id;
}
onCellInput(row: any, newVal: string): void {
row.value = newVal;
}
}
<cdk-virtual-scroll-viewport itemSize="40" class="viewport">
<table>
<tr *cdkVirtualFor="let row of rows; trackBy: trackById">
<td>
<input
[value]="row.value"
(input)="onCellInput(row, $event.target.value)">
</td>
</tr>
</table>
</cdk-virtual-scroll-viewport>
Para não atirar no próprio pé
itemSize deve corresponder à altura da linha (inclua padding/border).
cdkVirtualFor substitui o *ngFor, ele cuida da reciclagem dos nós.
Mesmo com Virtual Scroll, mantenha o onCellInput manual, isso evita o ciclo de detecção automático em cada tecla.
Mas a real é que o Virtual Scroll resolve cerca de 80 % dos problemas. Se você está pulando direto para Canvas antes de tentar virtualizar, está complicando a própria vida a toa.
A cirurgia (Detaching Change Detector)
Quando precisar de atualização em lote (colagem, cálculos pesados), desligue o detector e force a renderização apenas quando for realmente necessário.
import { ChangeDetectorRef, Component } from '@angular/core';
@Component({
selector: 'app-batch-grid',
templateUrl: './batch-grid.component.html',
changeDetection: ChangeDetectionStrategy.OnPush
})
export class BatchGridComponent {
rows = Array.from({ length: 5000 }, (_, i) => ({ id: i, value: '' }));
private bulkMode = false;
constructor(private cd: ChangeDetectorRef) {}
startBulk(): void {
this.cd.detach(); // nada será detectado automaticamente
this.bulkMode = true;
}
finishBulk(): void {
// …processamento pesado…
this.cd.detectChanges(); // única passagem
this.cd.reattach(); // volta ao normal
this.bulkMode = false;
}
onCellInput(row: any, val: string): void {
row.value = val;
if (!this.bulkMode) {
this.cd.detectChanges(); // atualização pontual
}
}
}
- Use
startBulk()antes de operações massivas (colagem, importação). finishBulk()faz o cálculo e dispara uma única passagem de CD.
Num projeto interno de ERP que peguei com 8 mil linhas, a tela levava uns 1.8s só pra responder a cada colagem do usuário. Só de meter o detach ali, o tempo caiu pra uns 200 ms. Foi bizarro!
Chutando o balde (DOM direto / Canvas)
Se Virtual Scroll + detach ainda não dão conta, o último recurso é abandonar o Angular no trecho crítico e tocar o DOM “na unha”. Pode ser via Renderer2 ou, melhor ainda, usando a API de Canvas para desenhar a grade.
import { AfterViewInit, Component, ElementRef, ViewChild } from '@angular/core';
@Component({
selector: 'app-canvas-grid',
template: `<canvas #gridCanvas width="1200" height="800"></canvas>`
})
export class CanvasGridComponent implements AfterViewInit {
@ViewChild('gridCanvas') canvas!: ElementRef<HTMLCanvasElement>;
ngAfterViewInit(): void {
const ctx = this.canvas.nativeElement.getContext('2d')!;
this.drawInitialGrid(ctx);
}
drawInitialGrid(ctx: CanvasRenderingContext2D): void {
const cellSize = 20;
for (let r = 0; r < 5000; r++) {
for (let c = 0; c < 10; c++) {
ctx.strokeRect(c * cellSize, r * cellSize, cellSize, cellSize);
}
}
}
// Atualiza só a célula alterada
updateCell(row: number, col: number, value: string): void {
const ctx = this.canvas.nativeElement.getContext('2d')!;
const cellSize = 20;
ctx.clearRect(col * cellSize, row * cellSize, cellSize, cellSize);
ctx.fillText(value, col * cellSize + 2, row * cellSize + 15);
}
}
Quando usar?
Dados tabulares simples (texto, cores) que precisam de scroll fluido. Cenários onde a latência de criação/destruição de nós ultrapassa o ganho de interatividade.
❌ O que NÃO fazer
<tr *ngFor="let row of rows; trackBy: trackById">
<td>{{ heavyCalc(row) }}</td> <!-- roda a cada CD -->
</tr>
heavyCalc(row: any): string {
return expensiveService.compute(row.id); // chamada cara em cada ciclo
}
✅ O que FAZER
<tr *ngFor="let row of rows; trackBy: trackById">
<td>
<input
[value]="row.value"
(input)="onCellInput(row, $event.target.value)">
</td>
</tr>
onCellInput(row: any, val: string): void {
row.value = val;
this.cd.detectChanges(); // atualização controlada
}
No fim das contas, a regra é clara.. se o framework virou o gargalo, tire o framework da jogada no componente específico. Assuma o controle do DOM e deixe o Angular cuidar só do que importa.

Comentários