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.

La gestión eficiente de grandes listas de datos representa uno de los desafíos más comunes en el desarrollo frontend moderno. Cuando una aplicación necesita mostrar miles o incluso millones de elementos, los enfoques tradicionales de renderizado alcanzan rápidamente sus límites, provocando ralentizaciones significativas y una experiencia de usuario degradada.
La virtualización de listas constituye la solución de referencia para resolver este problema. Esta técnica consiste en renderizar únicamente los elementos visibles en el área de visualización (viewport), mientras se simula la presencia del conjunto completo de datos. El navegador manipula así solo una fracción del DOM, reduciendo drásticamente la carga de memoria y el tiempo de renderizado.
La virtualización puede reducir el número de elementos DOM de varios miles a unas pocas decenas, mejorando el rendimiento de renderizado en más del 90% en algunos casos.
Comprendiendo los Fundamentos de la Virtualización
El principio de la virtualización se basa en un concepto simple: ¿por qué renderizar 10,000 elementos si el usuario solo ve 20 a la vez? La virtualización calcula dinámicamente qué elementos deben ser visibles según la posición de desplazamiento, y luego renderiza únicamente esos elementos específicos.
Este enfoque requiere varios cálculos esenciales:
- La altura total estimada de la lista completa
- El índice del primer elemento visible
- El índice del último elemento visible
- El desplazamiento (offset) para posicionar correctamente los 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 };
}Implementación de una Lista Virtualizada con NestJS y WebSocket
En el contexto de una aplicación full-stack, la virtualización del lado del cliente debe acompañarse frecuentemente de una gestión inteligente de datos del lado del servidor. NestJS ofrece una arquitectura robusta para implementar este tipo de funcionalidad.
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[]> {
// Implementación de la consulta a base de datos
return [];
}
}Optimización del Renderizado con React y useDeferredValue
React 19 introduce mejoras significativas para la gestión de actualizaciones de prioridad. El hook useDeferredValue permite diferir actualizaciones menos críticas, evitando bloqueos de la interfaz durante el desplazamiento rápido.
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>
);
}Gestión de Alturas Variables
Las listas con elementos de diferentes alturas representan un desafío adicional. Un enfoque eficiente consiste en medir las alturas reales de los elementos y mantener un caché de estas mediciones.
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;
}
}Estrategias de Precarga y Almacenamiento en Caché
Para garantizar una experiencia de usuario fluida, la precarga de datos antes de que sean visibles resulta esencial. Esta técnica anticipa las necesidades del usuario cargando los datos con anticipación.
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);
}
}
}
}Optimización del Rendimiento con Web Workers
Los cálculos intensivos relacionados con la virtualización pueden trasladarse a Web Workers para liberar el hilo principal y mantener una interfaz reactiva.
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;
});
}Integración con Bibliotecas Existentes
Varias bibliotecas probadas facilitan la implementación de la virtualización. React Virtualized y TanStack Virtual ofrecen soluciones robustas y bien 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>
);
}Medición y Monitoreo del Rendimiento
La medición continua del rendimiento permite identificar cuellos de botella y optimizar en consecuencia. La API Performance del navegador ofrece herramientas valiosas para este monitoreo.
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();
}
}¿Listo para aprobar tus entrevistas de Node.js / NestJS?
Practica con nuestros simuladores interactivos, flashcards y tests técnicos.
Conclusión
La virtualización de listas representa una técnica indispensable para cualquier desarrollador que trabaje con grandes cantidades de datos. Los enfoques presentados en este artículo, desde la implementación básica hasta el uso de Web Workers, ofrecen un abanico de soluciones adaptadas a diferentes contextos.
Los puntos clave a recordar incluyen la importancia del cálculo preciso de los elementos visibles, la gestión inteligente del caché para la precarga, y el uso juicioso de las funcionalidades de React 19 como useDeferredValue. La integración con bibliotecas probadas como TanStack Virtual simplifica considerablemente la implementación mientras garantiza un rendimiento óptimo.
El dominio de estas técnicas de virtualización constituye una ventaja importante para las entrevistas técnicas, donde la capacidad de optimizar el rendimiento de una aplicación se evalúa frecuentemente. Estas habilidades demuestran una comprensión profunda de los mecanismos de renderizado y la gestión de memoria en aplicaciones JavaScript modernas.
¿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 7 de septiembre de 2026
Etiquetas
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.

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.

Entrevista NestJS: Guards, Interceptors y arquitectura modular
Preguntas frecuentes en entrevistas técnicas de NestJS sobre Guards, Interceptors y arquitectura modular, con ejemplos concretos en TypeScript y explicaciones técnicas.