# 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. - Published: 2026-09-07 - Updated: 2026-09-07 - Author: Anthony Fillion-Maillet - Tags: javascript, rendimiento, virtualizacion, react, nestjs - Reading time: 12 min --- 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 ```typescript 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. ```typescript 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 = new Map(); @SubscribeMessage('requestItems') async handleItemRequest( client: Socket, payload: PaginatedRequest ): Promise { 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 { // 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. ```typescript import React, { useDeferredValue, useMemo, useCallback, useRef, useEffect, useState } from 'react'; interface VirtualizedListProps { items: T[]; itemHeight: number; containerHeight: number; renderItem: (item: T, index: number) => React.ReactNode; overscan?: number; } function VirtualizedList({ items, itemHeight, containerHeight, renderItem, overscan = 3 }: VirtualizedListProps) { const [scrollTop, setScrollTop] = useState(0); const deferredScrollTop = useDeferredValue(scrollTop); const containerRef = useRef(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) => { 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 (
{visibleItems.map((item, index) => renderItem(item, visibleRange.startIndex + index) )}
); } ``` ## 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. ```typescript interface HeightCache { heights: Map; 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. ```typescript import { Injectable } from '@nestjs/common'; interface CacheEntry { data: T[]; timestamp: number; ttl: number; } @Injectable() export class PrefetchService { private cache: Map> = new Map(); private readonly DEFAULT_TTL = 60000; // 1 minuto async prefetchRange( fetcher: (start: number, end: number) => Promise, currentIndex: number, bufferSize: number = 50 ): Promise { 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(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. ```typescript // virtualization.worker.ts 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(items: T[], query: string): T[] { return items.filter(item => JSON.stringify(item).toLowerCase().includes(query.toLowerCase()) ); } function sortItems(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. ```typescript 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(null); const virtualizer = useVirtualizer({ count: items.length, getScrollElement: () => parentRef.current, estimateSize: () => 50, overscan: 5, }); return (
{virtualizer.getVirtualItems().map((virtualItem) => (

{items[virtualItem.index].title}

{items[virtualItem.index].description}

))}
); } ``` ## 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. ```typescript class PerformanceMonitor { private marks: Map = new Map(); private measurements: Map = 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 { const report: Record = {}; 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(); } } ``` ## 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. --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/es/blog/node-nestjs/high-performance-javascript-listview-virtualization