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.

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.
// 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.
// 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.
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.
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.
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.
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.
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.
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.
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.
¿Sabrías detectar el bug en Node.js / NestJS?
Un fragmento real, un bug oculto, un intento al día. Sin cuenta para probar.

Escrito por
Anthony Fillion-MailletFundador 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

NestJS y Redis en 2026: Caché, Sesiones y Preguntas de Entrevista
Guía completa sobre la integración de Redis con NestJS: configuración del cache-manager, gestión de sesiones distribuidas y preguntas técnicas para entrevistas de desarrollador.

ListView JavaScript de Alto Rendimiento en 2026: Virtualización y Técnicas de Optimización
Guía completa sobre la implementación de listas virtualizadas de alto rendimiento en JavaScript. Técnicas de virtualización, optimización del renderizado y mejores prácticas para manejar miles de elementos sin pérdida de rendimiento.

NestJS y MongoDB en 2026: Mongoose, Agregaciones y Preguntas de Entrevista
Guía completa sobre la integración de NestJS 12 con MongoDB mediante Mongoose 9. Aprende sobre esquemas, pipelines de agregación y preguntas técnicas frecuentes en entrevistas.