ListView JavaScript de Alta Performance em 2026: Virtualização e Técnicas de Otimização
Guia completo sobre a implementação de listas virtualizadas de alta performance em JavaScript. Técnicas de virtualização, otimização de renderização e melhores práticas para gerenciar milhares de elementos sem perda de desempenho.

O gerenciamento eficiente de grandes listas de dados representa um dos desafios mais comuns no desenvolvimento frontend moderno. Quando uma aplicação precisa exibir milhares ou até milhões de elementos, as abordagens tradicionais de renderização rapidamente atingem seus limites, provocando lentidões significativas e uma experiência de usuário degradada.
A virtualização de listas constitui a solução de referência para resolver esse problema. Essa técnica consiste em renderizar apenas os elementos visíveis na área de visualização (viewport), enquanto simula a presença do conjunto completo de dados. O navegador manipula assim apenas uma fração do DOM, reduzindo drasticamente a carga de memória e o tempo de renderização.
A virtualização pode reduzir o número de elementos DOM de vários milhares para algumas dezenas, melhorando o desempenho de renderização em mais de 90% em alguns casos.
Compreendendo os Fundamentos da Virtualização
O princípio da virtualização baseia-se em um conceito simples: por que renderizar 10.000 elementos se o usuário só visualiza 20 por vez? A virtualização calcula dinamicamente quais elementos devem estar visíveis de acordo com a posição de rolagem, e então renderiza apenas esses elementos específicos.
Essa abordagem requer vários cálculos essenciais:
- A altura total estimada da lista completa
- O índice do primeiro elemento visível
- O índice do último elemento visível
- O deslocamento (offset) para posicionar corretamente os elementos
interface VirtualizedListConfig {
itemHeight: number;
containerHeight: number;
overscan: number;
totalItems: number;
}
function calculateVisibleRange(
scrollTop: number,
config: VirtualizedListConfig
): { startIndex: number; endIndex: number; offsetY: number } {
const { itemHeight, containerHeight, overscan, totalItems } = config;
const startIndex = Math.max(0, Math.floor(scrollTop / itemHeight) - overscan);
const visibleCount = Math.ceil(containerHeight / itemHeight);
const endIndex = Math.min(totalItems - 1, startIndex + visibleCount + overscan * 2);
const offsetY = startIndex * itemHeight;
return { startIndex, endIndex, offsetY };
}Implementação de uma Lista Virtualizada com NestJS e WebSocket
No contexto de uma aplicação full-stack, a virtualização do lado do cliente frequentemente deve ser acompanhada de um gerenciamento inteligente de dados do lado do servidor. O NestJS oferece uma arquitetura robusta para implementar esse tipo de funcionalidade.
import { Injectable } from '@nestjs/common';
import { WebSocketGateway, WebSocketServer, SubscribeMessage } from '@nestjs/websockets';
import { Server, Socket } from 'socket.io';
interface PaginatedRequest {
startIndex: number;
endIndex: number;
searchQuery?: string;
}
@Injectable()
@WebSocketGateway({ cors: true })
export class VirtualizedListGateway {
@WebSocketServer()
server: Server;
private dataCache: Map<string, any[]> = new Map();
@SubscribeMessage('requestItems')
async handleItemRequest(
client: Socket,
payload: PaginatedRequest
): Promise<void> {
const { startIndex, endIndex, searchQuery } = payload;
const cacheKey = `${searchQuery || 'all'}_${startIndex}_${endIndex}`;
if (this.dataCache.has(cacheKey)) {
client.emit('itemsResponse', {
items: this.dataCache.get(cacheKey),
startIndex,
endIndex,
cached: true
});
return;
}
const items = await this.fetchItemsFromDatabase(startIndex, endIndex, searchQuery);
this.dataCache.set(cacheKey, items);
client.emit('itemsResponse', {
items,
startIndex,
endIndex,
cached: false
});
}
private async fetchItemsFromDatabase(
start: number,
end: number,
query?: string
): Promise<any[]> {
// Implementação da consulta ao banco de dados
return [];
}
}Otimização da Renderização com React e useDeferredValue
O React 19 introduz melhorias significativas para o gerenciamento de atualizações de prioridade. O hook useDeferredValue permite adiar atualizações menos críticas, evitando bloqueios da interface durante a rolagem rápida.
import React, { useDeferredValue, useMemo, useCallback, useRef, useEffect, useState } from 'react';
interface VirtualizedListProps<T> {
items: T[];
itemHeight: number;
containerHeight: number;
renderItem: (item: T, index: number) => React.ReactNode;
overscan?: number;
}
function VirtualizedList<T>({
items,
itemHeight,
containerHeight,
renderItem,
overscan = 3
}: VirtualizedListProps<T>) {
const [scrollTop, setScrollTop] = useState(0);
const deferredScrollTop = useDeferredValue(scrollTop);
const containerRef = useRef<HTMLDivElement>(null);
const visibleRange = useMemo(() => {
const startIndex = Math.max(0, Math.floor(deferredScrollTop / itemHeight) - overscan);
const visibleCount = Math.ceil(containerHeight / itemHeight);
const endIndex = Math.min(items.length - 1, startIndex + visibleCount + overscan * 2);
return { startIndex, endIndex };
}, [deferredScrollTop, itemHeight, containerHeight, items.length, overscan]);
const handleScroll = useCallback((e: React.UIEvent<HTMLDivElement>) => {
setScrollTop(e.currentTarget.scrollTop);
}, []);
const visibleItems = useMemo(() => {
return items.slice(visibleRange.startIndex, visibleRange.endIndex + 1);
}, [items, visibleRange]);
const totalHeight = items.length * itemHeight;
const offsetY = visibleRange.startIndex * itemHeight;
return (
<div
ref={containerRef}
style={{ height: containerHeight, overflow: 'auto' }}
onScroll={handleScroll}
>
<div style={{ height: totalHeight, position: 'relative' }}>
<div style={{ transform: `translateY(${offsetY}px)` }}>
{visibleItems.map((item, index) =>
renderItem(item, visibleRange.startIndex + index)
)}
</div>
</div>
</div>
);
}Gerenciamento de Alturas Variáveis
Listas com elementos de alturas diferentes representam um desafio adicional. Uma abordagem eficiente consiste em medir as alturas reais dos elementos e manter um cache dessas medições.
interface HeightCache {
heights: Map<number, number>;
estimatedHeight: number;
totalHeight: number;
}
class DynamicHeightManager {
private cache: HeightCache = {
heights: new Map(),
estimatedHeight: 50,
totalHeight: 0
};
constructor(private totalItems: number, estimatedHeight: number = 50) {
this.cache.estimatedHeight = estimatedHeight;
this.recalculateTotalHeight();
}
setItemHeight(index: number, height: number): void {
const previousHeight = this.cache.heights.get(index) || this.cache.estimatedHeight;
this.cache.heights.set(index, height);
this.cache.totalHeight += height - previousHeight;
}
getItemOffset(index: number): number {
let offset = 0;
for (let i = 0; i < index; i++) {
offset += this.cache.heights.get(i) || this.cache.estimatedHeight;
}
return offset;
}
getItemHeight(index: number): number {
return this.cache.heights.get(index) || this.cache.estimatedHeight;
}
getTotalHeight(): number {
return this.cache.totalHeight;
}
findIndexAtOffset(offset: number): number {
let currentOffset = 0;
for (let i = 0; i < this.totalItems; i++) {
const height = this.getItemHeight(i);
if (currentOffset + height > offset) {
return i;
}
currentOffset += height;
}
return this.totalItems - 1;
}
private recalculateTotalHeight(): void {
this.cache.totalHeight = this.totalItems * this.cache.estimatedHeight;
}
}Estratégias de Pré-carregamento e Cache
Para garantir uma experiência de usuário fluida, o pré-carregamento de dados antes de serem visíveis é essencial. Essa técnica antecipa as necessidades do usuário carregando os dados com antecedência.
import { Injectable } from '@nestjs/common';
interface CacheEntry<T> {
data: T[];
timestamp: number;
ttl: number;
}
@Injectable()
export class PrefetchService {
private cache: Map<string, CacheEntry<any>> = new Map();
private readonly DEFAULT_TTL = 60000; // 1 minuto
async prefetchRange<T>(
fetcher: (start: number, end: number) => Promise<T[]>,
currentIndex: number,
bufferSize: number = 50
): Promise<void> {
const prefetchStart = Math.max(0, currentIndex - bufferSize);
const prefetchEnd = currentIndex + bufferSize;
const cacheKey = `${prefetchStart}_${prefetchEnd}`;
if (this.isValid(cacheKey)) {
return;
}
const data = await fetcher(prefetchStart, prefetchEnd);
this.cache.set(cacheKey, {
data,
timestamp: Date.now(),
ttl: this.DEFAULT_TTL
});
}
get<T>(start: number, end: number): T[] | null {
const cacheKey = `${start}_${end}`;
const entry = this.cache.get(cacheKey);
if (entry && this.isValid(cacheKey)) {
return entry.data as T[];
}
return null;
}
private isValid(key: string): boolean {
const entry = this.cache.get(key);
if (!entry) return false;
return Date.now() - entry.timestamp < entry.ttl;
}
clearExpired(): void {
const now = Date.now();
for (const [key, entry] of this.cache.entries()) {
if (now - entry.timestamp >= entry.ttl) {
this.cache.delete(key);
}
}
}
}Otimização de Performance com Web Workers
Cálculos intensivos relacionados à virtualização podem ser transferidos para Web Workers para liberar a thread principal e manter uma interface responsiva.
self.onmessage = function(e: MessageEvent) {
const { type, payload } = e.data;
switch (type) {
case 'CALCULATE_VISIBLE_RANGE':
const result = calculateRange(payload);
self.postMessage({ type: 'RANGE_CALCULATED', result });
break;
case 'FILTER_ITEMS':
const filtered = filterItems(payload.items, payload.query);
self.postMessage({ type: 'ITEMS_FILTERED', result: filtered });
break;
case 'SORT_ITEMS':
const sorted = sortItems(payload.items, payload.comparator);
self.postMessage({ type: 'ITEMS_SORTED', result: sorted });
break;
}
};
function calculateRange(payload: {
scrollTop: number;
itemHeight: number;
containerHeight: number;
totalItems: number;
overscan: number;
}) {
const { scrollTop, itemHeight, containerHeight, totalItems, overscan } = payload;
const startIndex = Math.max(0, Math.floor(scrollTop / itemHeight) - overscan);
const visibleCount = Math.ceil(containerHeight / itemHeight);
const endIndex = Math.min(totalItems - 1, startIndex + visibleCount + overscan * 2);
return { startIndex, endIndex, offsetY: startIndex * itemHeight };
}
function filterItems<T>(items: T[], query: string): T[] {
return items.filter(item =>
JSON.stringify(item).toLowerCase().includes(query.toLowerCase())
);
}
function sortItems<T>(items: T[], comparatorKey: keyof T): T[] {
return [...items].sort((a, b) => {
if (a[comparatorKey] < b[comparatorKey]) return -1;
if (a[comparatorKey] > b[comparatorKey]) return 1;
return 0;
});
}Integração com Bibliotecas Existentes
Várias bibliotecas comprovadas facilitam a implementação da virtualização. React Virtualized e TanStack Virtual oferecem soluções robustas e bem documentadas.
import { useVirtualizer } from '@tanstack/react-virtual';
import { useRef } from 'react';
interface Item {
id: string;
title: string;
description: string;
}
function TanStackVirtualList({ items }: { items: Item[] }) {
const parentRef = useRef<HTMLDivElement>(null);
const virtualizer = useVirtualizer({
count: items.length,
getScrollElement: () => parentRef.current,
estimateSize: () => 50,
overscan: 5,
});
return (
<div
ref={parentRef}
style={{ height: '400px', overflow: 'auto' }}
>
<div
style={{
height: `${virtualizer.getTotalSize()}px`,
width: '100%',
position: 'relative',
}}
>
{virtualizer.getVirtualItems().map((virtualItem) => (
<div
key={virtualItem.key}
style={{
position: 'absolute',
top: 0,
left: 0,
width: '100%',
height: `${virtualItem.size}px`,
transform: `translateY(${virtualItem.start}px)`,
}}
>
<div className="p-4 border-b">
<h3>{items[virtualItem.index].title}</h3>
<p>{items[virtualItem.index].description}</p>
</div>
</div>
))}
</div>
</div>
);
}Medição e Monitoramento de Performance
A medição contínua de performance permite identificar gargalos e otimizar adequadamente. A API Performance do navegador oferece ferramentas valiosas para esse monitoramento.
class PerformanceMonitor {
private marks: Map<string, number> = new Map();
private measurements: Map<string, number[]> = new Map();
startMeasure(name: string): void {
this.marks.set(name, performance.now());
}
endMeasure(name: string): number {
const start = this.marks.get(name);
if (!start) return 0;
const duration = performance.now() - start;
if (!this.measurements.has(name)) {
this.measurements.set(name, []);
}
this.measurements.get(name)!.push(duration);
return duration;
}
getAverageTime(name: string): number {
const times = this.measurements.get(name);
if (!times || times.length === 0) return 0;
return times.reduce((a, b) => a + b, 0) / times.length;
}
getReport(): Record<string, { average: number; min: number; max: number; count: number }> {
const report: Record<string, any> = {};
for (const [name, times] of this.measurements.entries()) {
report[name] = {
average: this.getAverageTime(name),
min: Math.min(...times),
max: Math.max(...times),
count: times.length
};
}
return report;
}
reset(): void {
this.marks.clear();
this.measurements.clear();
}
}Pronto para mandar bem nas entrevistas de Node.js / NestJS?
Pratique com nossos simuladores interativos, flashcards e testes tecnicos.
Conclusão
A virtualização de listas representa uma técnica indispensável para qualquer desenvolvedor que trabalhe com grandes quantidades de dados. As abordagens apresentadas neste artigo, desde a implementação básica até o uso de Web Workers, oferecem um leque de soluções adaptadas a diferentes contextos.
Os pontos-chave a lembrar incluem a importância do cálculo preciso dos elementos visíveis, o gerenciamento inteligente de cache para pré-carregamento, e o uso criterioso das funcionalidades do React 19 como useDeferredValue. A integração com bibliotecas comprovadas como TanStack Virtual simplifica consideravelmente a implementação enquanto garante performance otimizada.
O domínio dessas técnicas de virtualização constitui uma vantagem importante para entrevistas técnicas, onde a capacidade de otimizar a performance de uma aplicação é frequentemente avaliada. Essas habilidades demonstram uma compreensão profunda dos mecanismos de renderização e do gerenciamento de memória em aplicações JavaScript modernas.
Você saberia encontrar o bug em Node.js / NestJS?
Um trecho real, um bug escondido, uma tentativa por dia. Sem conta para testar.

Escrito por
Anthony Fillion-MailletFundador da SharpSkill
Desenvolvedor fullstack há mais de 10 anos. Dirige a SharpSkill e responde por tudo o que é publicado aqui.
Atualizado em 7 de setembro de 2026
Tags
Compartilhar
Artigos relacionados

NestJS e Redis em 2026: Cache, Sessões e Perguntas de Entrevista
Guia completo sobre a integração do Redis com NestJS: configuração do cache-manager, gerenciamento de sessões distribuídas e perguntas técnicas para entrevistas de desenvolvedor.

NestJS e MongoDB em 2026: Mongoose, Agregações e Perguntas de Entrevista
Guia completo sobre a integração do NestJS 12 com MongoDB através do Mongoose 9. Aprenda sobre schemas, pipelines de agregação e perguntas técnicas frequentes em entrevistas.

Entrevista NestJS: Guards, Interceptors e arquitetura modular
Perguntas frequentes em entrevistas técnicas de NestJS sobre Guards, Interceptors e arquitetura modular, com exemplos concretos em TypeScript e explicações técnicas.