ListView JavaScript de Alto Rendimiento en 2026: Windowing, Virtualizacion y Optimizacion

Domina las tecnicas de virtualizacion y windowing para crear listas JavaScript ultrarapidas. Guia completa con React y NestJS.

ListView JavaScript de Alto Rendimiento en 2026: Windowing, Virtualizacion y Optimizacion

La visualizacion de grandes volumenes de datos en aplicaciones web representa uno de los desafios mas significativos para los desarrolladores JavaScript. Ya sea que se trate de miles de productos en un catalogo de comercio electronico o millones de registros en un panel analitico, un enfoque ingenuo del renderizado inevitablemente genera problemas de rendimiento. La virtualizacion y el windowing constituyen las soluciones modernas para abordar estas problematicas.

La virtualizacion de listas puede reducir la cantidad de elementos DOM de varios miles a unas pocas decenas, mejorando drasticamente los tiempos de renderizado y el consumo de memoria.

Comprendiendo el Problema de Rendimiento en Listas

Cuando una aplicacion JavaScript renderiza una lista de 10,000 elementos, cada elemento genera nodos DOM. El navegador debe calcular estilos, realizar el layout y pintar cada elemento. Esta operacion consume memoria y tiempo de CPU, provocando congelamientos y una experiencia de usuario degradada.

El problema se agrava con la complejidad de los elementos. Un texto simple en un <li> tiene un impacto limitado, pero una tarjeta de producto con imagen, descripcion y botones multiplica los costos de renderizado.

typescript
// Enfoque ingenuo - Evitar con 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>
  );
}

El Principio del Windowing

El windowing, o ventaneo, consiste en renderizar unicamente los elementos visibles en la zona de visualizacion (viewport) mas un buffer de algunos elementos arriba y abajo. Los elementos fuera de pantalla se destruyen y recrean dinamicamente durante el desplazamiento.

Esta tecnica reduce el numero de nodos DOM activos en varios ordenes de magnitud. Una lista de 100,000 elementos puede funcionar con solo 20 a 50 nodos DOM visibles.

typescript
// Configuracion basica para windowing
interface WindowConfig {
  itemHeight: number;
  containerHeight: number;
  overscan: number; // Elementos adicionales renderizados fuera de pantalla
}

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 };
}

Implementacion con React Window

React Window representa la biblioteca de referencia para la virtualizacion de listas en React. Ligera y performante, gestiona automaticamente el calculo de posiciones y el reciclaje 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>{product.price.toFixed(2)} USD</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 con Altura Variable

Las listas reales frecuentemente contienen elementos de alturas diferentes. React Window ofrece el componente VariableSizeList para manejar esta situacion.

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)!;
    }
    
    // Estimacion basada en la longitud del contenido
    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 Carga Paginada

La virtualizacion del lado del cliente funciona de manera optima con un backend que soporte paginacion. NestJS permite implementar facilmente endpoints paginados de alto rendimiento.

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 con Virtualizacion

El scroll infinito combina la virtualizacion con la carga progresiva de datos. Este enfoque ofrece la mejor experiencia de usuario para colecciones 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">
                  Cargando...
                </div>
              );
            }
            
            return (
              <ProductRow
                product={allProducts[index]}
                style={style}
              />
            );
          }}
        </List>
      )}
    </InfiniteLoader>
  );
}

Optimizacion del Rendimiento de Renderizado

Varias tecnicas complementarias mejoran el rendimiento de la virtualizacion.

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

// Memoizacion agresiva de componentes de fila
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>{product.price.toFixed(2)} USD</span>
    </div>
  );
}, areEqual);

// Hook personalizado para gestion de scroll optimizada
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]);
}

Grillas Virtualizadas

Las grillas bidimensionales requieren un enfoque de virtualizacion bidireccional. React Window proporciona el componente FixedSizeGrid para este 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>
  );
}

Medicion del Rendimiento

La medicion precisa del rendimiento permite validar la efectividad de la virtualizacion e identificar cuellos de botella.

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;
  });
}

// Servicio NestJS para monitoreo de rendimiento
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);
    
    // Conservar solo las ultimas 100 mediciones
    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;
  }
}

¿Listo para aprobar tus entrevistas de Node.js / NestJS?

Practica con nuestros simuladores interactivos, flashcards y tests técnicos.

Conclusion

La virtualizacion de listas representa una tecnica fundamental para las aplicaciones JavaScript modernas que manejan grandes volumenes de datos. Combinando el windowing del lado del cliente con una paginacion eficiente del lado del servidor, los desarrolladores pueden crear interfaces fluidas capaces de mostrar millones de elementos sin comprometer el rendimiento.

Herramientas como React Window simplifican considerablemente la implementacion de estos patrones. La integracion con TanStack Query para scroll infinito y NestJS para el backend crea una arquitectura completa y de alto rendimiento. La clave del exito radica en la medicion continua del rendimiento y la optimizacion iterativa basada en datos reales.

Reto diario

¿Sabrías detectar el bug en Node.js / NestJS?

Un fragmento real, un bug oculto, un intento al día. Sin cuenta para probar.

Anthony Fillion-Maillet

Escrito por

Anthony Fillion-Maillet

Fundador de SharpSkill

Desarrollador fullstack desde hace más de 10 años. Dirige SharpSkill y responde por todo lo que se publica aquí.

Actualizado el 15 de septiembre de 2026

Compartir

Artículos relacionados