ListView JavaScript de Alta Performance em 2026: Windowing, Virtualizacao e Otimizacao

Domine as tecnicas de virtualizacao e windowing para criar listas JavaScript ultraperformantes. Guia completo com React e NestJS.

ListView JavaScript de Alta Performance em 2026: Windowing, Virtualizacao e Otimizacao

A exibicao de grandes volumes de dados em aplicacoes web representa um dos desafios mais significativos para desenvolvedores JavaScript. Seja lidando com milhares de produtos em um catalogo de e-commerce ou milhoes de registros em um painel analitico, uma abordagem ingenuagenuina de renderizacao inevitavelmente gera problemas de performance. A virtualizacao e o windowing constituem as solucoes modernas para resolver essas problematicas.

A virtualizacao de listas pode reduzir a quantidade de elementos DOM de varios milhares para apenas algumas dezenas, melhorando drasticamente os tempos de renderizacao e o consumo de memoria.

Compreendendo o Problema de Performance em Listas

Quando uma aplicacao JavaScript renderiza uma lista de 10.000 elementos, cada elemento gera nos DOM. O navegador precisa calcular estilos, realizar o layout e pintar cada elemento. Essa operacao consome memoria e tempo de CPU, provocando travamentos e uma experiencia de usuario degradada.

O problema se agrava com a complexidade dos elementos. Um texto simples em um <li> tem impacto limitado, mas um card de produto com imagem, descricao e botoes multiplica os custos de renderizacao.

typescript
// Abordagem ingenuagenuina - Evitar com listas grandes
interface Product {
  id: string;
  name: string;
  price: number;
  imageUrl: string;
}

function ProductList({ products }: { products: Product[] }) {
  return (
    <ul>
      {products.map((product) => (
        <li key={product.id}>
          <ProductCard product={product} />
        </li>
      ))}
    </ul>
  );
}

O Principio do Windowing

O windowing, ou janelamento, consiste em renderizar apenas os elementos visiveis na zona de visualizacao (viewport) mais um buffer de alguns elementos acima e abaixo. Os elementos fora da tela sao destruidos e recriados dinamicamente durante a rolagem.

Essa tecnica reduz o numero de nos DOM ativos em varias ordens de grandeza. Uma lista de 100.000 elementos pode funcionar com apenas 20 a 50 nos DOM visiveis.

typescript
// Configuracao basica para windowing
interface WindowConfig {
  itemHeight: number;
  containerHeight: number;
  overscan: number; // Elementos adicionais renderizados fora da tela
}

function calculateVisibleRange(
  scrollTop: number,
  config: WindowConfig,
  totalItems: number
): { startIndex: number; endIndex: number } {
  const { itemHeight, containerHeight, overscan } = 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
  );
  
  return { startIndex, endIndex };
}

Implementacao com React Window

React Window representa a biblioteca de referencia para virtualizacao de listas em React. Leve e performante, ela gerencia automaticamente o calculo de posicoes e a reciclagem de elementos.

typescript
import { FixedSizeList as List } from 'react-window';
import { memo, useCallback } from 'react';

interface RowProps {
  index: number;
  style: React.CSSProperties;
  data: Product[];
}

const Row = memo(function Row({ index, style, data }: RowProps) {
  const product = data[index];
  
  return (
    <div style={style} className="product-row">
      <img 
        src={product.imageUrl} 
        alt={product.name}
        loading="lazy"
      />
      <span>{product.name}</span>
      <span>R$ {product.price.toFixed(2)}</span>
    </div>
  );
});

function VirtualizedProductList({ products }: { products: Product[] }) {
  const itemData = products;
  
  return (
    <List
      height={600}
      itemCount={products.length}
      itemSize={80}
      width="100%"
      itemData={itemData}
    >
      {Row}
    </List>
  );
}

Listas com Altura Variavel

Listas reais frequentemente contem elementos de alturas diferentes. React Window oferece o componente VariableSizeList para lidar com essa situacao.

typescript
import { VariableSizeList as List } from 'react-window';
import { useRef, useCallback } from 'react';

interface Message {
  id: string;
  content: string;
  author: string;
}

function ChatMessageList({ messages }: { messages: Message[] }) {
  const listRef = useRef<List>(null);
  
  // Cache de alturas calculadas
  const heightCache = useRef<Map<number, number>>(new Map());
  
  const getItemHeight = useCallback((index: number): number => {
    if (heightCache.current.has(index)) {
      return heightCache.current.get(index)!;
    }
    
    // Estimativa baseada no comprimento do conteudo
    const message = messages[index];
    const baseHeight = 60;
    const charsPerLine = 50;
    const lineHeight = 24;
    const extraLines = Math.ceil(message.content.length / charsPerLine);
    
    const height = baseHeight + (extraLines - 1) * lineHeight;
    heightCache.current.set(index, height);
    
    return height;
  }, [messages]);
  
  const handleContentResize = useCallback((index: number, height: number) => {
    heightCache.current.set(index, height);
    listRef.current?.resetAfterIndex(index);
  }, []);
  
  return (
    <List
      ref={listRef}
      height={500}
      itemCount={messages.length}
      itemSize={getItemHeight}
      width="100%"
    >
      {({ index, style }) => (
        <MessageRow
          message={messages[index]}
          style={style}
          onResize={(height) => handleContentResize(index, height)}
        />
      )}
    </List>
  );
}

Backend NestJS para Carregamento Paginado

A virtualizacao do lado do cliente funciona de forma otima com um backend que suporte paginacao. NestJS permite implementar facilmente endpoints paginados de alta performance.

typescript
import { Controller, Get, Query } from '@nestjs/common';
import { ProductService } from './product.service';

interface PaginationQuery {
  offset: number;
  limit: number;
  search?: string;
}

interface PaginatedResponse<T> {
  data: T[];
  total: number;
  offset: number;
  limit: number;
}

@Controller('products')
export class ProductController {
  constructor(private readonly productService: ProductService) {}
  
  @Get()
  async getProducts(
    @Query('offset') offset = 0,
    @Query('limit') limit = 50,
    @Query('search') search?: string
  ): Promise<PaginatedResponse<Product>> {
    const [data, total] = await Promise.all([
      this.productService.findMany({
        skip: Number(offset),
        take: Math.min(Number(limit), 100), // Limite maximo
        where: search
          ? { name: { contains: search, mode: 'insensitive' } }
          : undefined,
      }),
      this.productService.count({
        where: search
          ? { name: { contains: search, mode: 'insensitive' } }
          : undefined,
      }),
    ]);
    
    return {
      data,
      total,
      offset: Number(offset),
      limit: Number(limit),
    };
  }
}

Scroll Infinito com Virtualizacao

O scroll infinito combina a virtualizacao com o carregamento progressivo de dados. Essa abordagem oferece a melhor experiencia de usuario para colecoes grandes.

typescript
import { FixedSizeList as List } from 'react-window';
import InfiniteLoader from 'react-window-infinite-loader';
import { useInfiniteQuery } from '@tanstack/react-query';

function InfiniteProductList() {
  const {
    data,
    fetchNextPage,
    hasNextPage,
    isFetchingNextPage,
  } = useInfiniteQuery({
    queryKey: ['products'],
    queryFn: async ({ pageParam = 0 }) => {
      const response = await fetch(
        `/api/products?offset=${pageParam}&limit=50`
      );
      return response.json();
    },
    getNextPageParam: (lastPage) => {
      const nextOffset = lastPage.offset + lastPage.limit;
      return nextOffset < lastPage.total ? nextOffset : undefined;
    },
    initialPageParam: 0,
  });
  
  const allProducts = data?.pages.flatMap((page) => page.data) ?? [];
  const totalCount = data?.pages[0]?.total ?? 0;
  
  const isItemLoaded = (index: number) => {
    return !hasNextPage || index < allProducts.length;
  };
  
  const loadMoreItems = isFetchingNextPage
    ? () => Promise.resolve()
    : () => fetchNextPage();
  
  return (
    <InfiniteLoader
      isItemLoaded={isItemLoaded}
      itemCount={hasNextPage ? allProducts.length + 1 : allProducts.length}
      loadMoreItems={loadMoreItems}
    >
      {({ onItemsRendered, ref }) => (
        <List
          ref={ref}
          height={600}
          itemCount={totalCount}
          itemSize={80}
          width="100%"
          onItemsRendered={onItemsRendered}
        >
          {({ index, style }) => {
            if (!isItemLoaded(index)) {
              return (
                <div style={style} className="loading-placeholder">
                  Carregando...
                </div>
              );
            }
            
            return (
              <ProductRow
                product={allProducts[index]}
                style={style}
              />
            );
          }}
        </List>
      )}
    </InfiniteLoader>
  );
}

Otimizacao da Performance de Renderizacao

Varias tecnicas complementares melhoram a performance da virtualizacao.

typescript
import { memo, useMemo, useCallback, useRef } from 'react';
import { areEqual } from 'react-window';

// Memoizacao agressiva de componentes de linha
const ProductRow = memo(function ProductRow({
  product,
  style,
  onSelect,
}: {
  product: Product;
  style: React.CSSProperties;
  onSelect: (id: string) => void;
}) {
  const handleClick = useCallback(() => {
    onSelect(product.id);
  }, [product.id, onSelect]);
  
  return (
    <div style={style} onClick={handleClick}>
      <span>{product.name}</span>
      <span>R$ {product.price.toFixed(2)}</span>
    </div>
  );
}, areEqual);

// Hook personalizado para gerenciamento de scroll otimizado
function useOptimizedScroll(
  onScroll: (scrollTop: number) => void,
  delay = 16
) {
  const lastScrollTop = useRef(0);
  const ticking = useRef(false);
  
  return useCallback((event: React.UIEvent<HTMLDivElement>) => {
    const scrollTop = event.currentTarget.scrollTop;
    lastScrollTop.current = scrollTop;
    
    if (!ticking.current) {
      requestAnimationFrame(() => {
        onScroll(lastScrollTop.current);
        ticking.current = false;
      });
      ticking.current = true;
    }
  }, [onScroll]);
}

Grids Virtualizados

Grids bidimensionais requerem uma abordagem de virtualizacao bidirecional. React Window fornece o componente FixedSizeGrid para esse caso de uso.

typescript
import { FixedSizeGrid as Grid } from 'react-window';

interface GridItem {
  id: string;
  thumbnailUrl: string;
  title: string;
}

function VirtualizedImageGrid({ items }: { items: GridItem[] }) {
  const columnCount = 4;
  const rowCount = Math.ceil(items.length / columnCount);
  
  const Cell = useCallback(
    ({
      columnIndex,
      rowIndex,
      style,
    }: {
      columnIndex: number;
      rowIndex: number;
      style: React.CSSProperties;
    }) => {
      const index = rowIndex * columnCount + columnIndex;
      
      if (index >= items.length) {
        return null;
      }
      
      const item = items[index];
      
      return (
        <div style={style} className="grid-cell">
          <img
            src={item.thumbnailUrl}
            alt={item.title}
            loading="lazy"
          />
          <span>{item.title}</span>
        </div>
      );
    },
    [items, columnCount]
  );
  
  return (
    <Grid
      columnCount={columnCount}
      columnWidth={200}
      height={600}
      rowCount={rowCount}
      rowHeight={250}
      width={800}
    >
      {Cell}
    </Grid>
  );
}

Medicao de Performance

A medicao precisa da performance permite validar a eficacia da virtualizacao e identificar gargalos.

typescript
import { useEffect, useRef } from 'react';

function useRenderMetrics(componentName: string) {
  const renderCount = useRef(0);
  const lastRenderTime = useRef(performance.now());
  
  useEffect(() => {
    const currentTime = performance.now();
    const timeSinceLastRender = currentTime - lastRenderTime.current;
    renderCount.current++;
    
    if (process.env.NODE_ENV === 'development') {
      console.log(
        `[${componentName}] Render #${renderCount.current}`,
        `Time since last: ${timeSinceLastRender.toFixed(2)}ms`
      );
    }
    
    lastRenderTime.current = currentTime;
  });
}

// Servico NestJS para monitoramento de performance
import { Injectable } from '@nestjs/common';

@Injectable()
export class PerformanceService {
  private metrics: Map<string, number[]> = new Map();
  
  recordQueryTime(queryName: string, duration: number): void {
    if (!this.metrics.has(queryName)) {
      this.metrics.set(queryName, []);
    }
    
    const times = this.metrics.get(queryName)!;
    times.push(duration);
    
    // Manter apenas as ultimas 100 medicoes
    if (times.length > 100) {
      times.shift();
    }
  }
  
  getAverageQueryTime(queryName: string): number {
    const times = this.metrics.get(queryName);
    if (!times || times.length === 0) return 0;
    
    return times.reduce((sum, t) => sum + t, 0) / times.length;
  }
}

Pronto para mandar bem nas entrevistas de Node.js / NestJS?

Pratique com nossos simuladores interativos, flashcards e testes tecnicos.

Conclusao

A virtualizacao de listas representa uma tecnica fundamental para aplicacoes JavaScript modernas que manipulam grandes volumes de dados. Combinando o windowing do lado do cliente com uma paginacao eficiente do lado do servidor, desenvolvedores podem criar interfaces fluidas capazes de exibir milhoes de elementos sem comprometer a performance.

Ferramentas como React Window simplificam consideravelmente a implementacao desses padroes. A integracao com TanStack Query para scroll infinito e NestJS para o backend cria uma arquitetura completa e de alta performance. A chave do sucesso reside na medicao continua da performance e na otimizacao iterativa baseada em dados reais.

Desafio do dia

Você saberia encontrar o bug em Node.js / NestJS?

Um trecho real, um bug escondido, uma tentativa por dia. Sem conta para testar.

Anthony Fillion-Maillet

Escrito por

Anthony Fillion-Maillet

Fundador da SharpSkill

Desenvolvedor fullstack há mais de 10 anos. Dirige a SharpSkill e responde por tudo o que é publicado aqui.

Atualizado em 15 de setembro de 2026

Compartilhar

Artigos relacionados