High Performance JavaScript ListView 2026: Teknik Virtualisasi dan Optimasi
Panduan lengkap teknik virtualisasi list JavaScript untuk performa tinggi di tahun 2026. Pelajari windowing, lazy loading, dan optimasi rendering untuk aplikasi skala besar.

Performa list view merupakan salah satu tantangan paling kritis dalam pengembangan aplikasi JavaScript modern. Ketika menangani ribuan hingga jutaan item data, pendekatan rendering tradisional dapat menyebabkan bottleneck yang signifikan, mempengaruhi responsivitas aplikasi dan pengalaman pengguna secara keseluruhan. Artikel ini membahas teknik-teknik virtualisasi dan optimasi terkini yang digunakan dalam pengembangan JavaScript profesional di tahun 2026.
Virtualisasi list dapat mengurangi penggunaan memori hingga 95% dan meningkatkan frame rate dari 10 FPS menjadi 60 FPS pada dataset besar. Teknik ini menjadi pertanyaan wajib dalam technical interview untuk posisi frontend senior di tahun 2026.
Memahami Masalah Performa List Rendering
Ketika sebuah aplikasi merender list dengan 10.000 item menggunakan pendekatan konvensional, browser harus membuat 10.000 DOM nodes, masing-masing dengan event listeners, styles, dan layout calculations. Proses ini mengkonsumsi memori yang besar dan menyebabkan jank yang terlihat saat scrolling.
Masalah utama terletak pada cara browser menangani DOM operations. Setiap penambahan node memerlukan reflow dan repaint, yang menjadi exponentially lebih lambat seiring bertambahnya jumlah element. Pada perangkat mobile dengan resource terbatas, masalah ini menjadi lebih pronounced.
// Contoh naive approach yang menyebabkan masalah performa
interface ListItem {
id: number;
title: string;
description: string;
}
function renderAllItems(items: ListItem[]): void {
const container = document.getElementById('list-container');
// Ini akan membuat 10.000 DOM nodes sekaligus
items.forEach(item => {
const element = document.createElement('div');
element.className = 'list-item';
element.innerHTML = `
<h3>${item.title}</h3>
<p>${item.description}</p>
`;
container?.appendChild(element);
});
}Prinsip Dasar Windowing dan Virtual Scrolling
Virtualisasi list bekerja dengan prinsip sederhana namun powerful: hanya merender item yang terlihat di viewport ditambah buffer kecil untuk smooth scrolling. Teknik ini dikenal sebagai windowing atau virtual scrolling.
Konsep kunci meliputi visible window yang merupakan area viewport yang terlihat oleh user, overscan yang merupakan buffer tambahan di atas dan bawah visible window untuk mencegah flickering, serta item pool yang merupakan DOM elements yang di-recycle untuk menghindari pembuatan dan penghapusan berulang.
interface VirtualListConfig {
itemHeight: number;
containerHeight: number;
overscan: number;
totalItems: number;
}
interface VisibleRange {
startIndex: number;
endIndex: number;
offsetY: number;
}
function calculateVisibleRange(
scrollTop: number,
config: VirtualListConfig
): VisibleRange {
const { itemHeight, containerHeight, overscan, totalItems } = config;
// Hitung index item pertama yang terlihat
const startIndex = Math.max(
0,
Math.floor(scrollTop / itemHeight) - overscan
);
// Hitung jumlah item yang muat dalam viewport
const visibleCount = Math.ceil(containerHeight / itemHeight);
// Hitung index item terakhir yang perlu dirender
const endIndex = Math.min(
totalItems - 1,
startIndex + visibleCount + overscan * 2
);
// Offset untuk memposisikan item dengan benar
const offsetY = startIndex * itemHeight;
return { startIndex, endIndex, offsetY };
}Implementasi Virtual List dengan React
Dalam ekosistem React, library seperti react-window dan react-virtualized telah menjadi standar industri. Namun memahami cara kerja internal virtualisasi sangat penting untuk technical interview dan debugging masalah performa.
import React, { useState, useCallback, useMemo, useRef } from 'react';
interface VirtualListProps<T> {
items: T[];
itemHeight: number;
containerHeight: number;
renderItem: (item: T, index: number) => React.ReactNode;
overscan?: number;
}
function VirtualList<T>({
items,
itemHeight,
containerHeight,
renderItem,
overscan = 3
}: VirtualListProps<T>): React.ReactElement {
const [scrollTop, setScrollTop] = useState(0);
const containerRef = useRef<HTMLDivElement>(null);
const totalHeight = items.length * itemHeight;
const visibleRange = useMemo(() => {
const startIndex = Math.max(
0,
Math.floor(scrollTop / itemHeight) - overscan
);
const visibleCount = Math.ceil(containerHeight / itemHeight);
const endIndex = Math.min(
items.length - 1,
startIndex + visibleCount + overscan * 2
);
return { startIndex, endIndex };
}, [scrollTop, itemHeight, containerHeight, items.length, overscan]);
const handleScroll = useCallback((e: React.UIEvent<HTMLDivElement>) => {
setScrollTop(e.currentTarget.scrollTop);
}, []);
const visibleItems = useMemo(() => {
const result: React.ReactNode[] = [];
for (let i = visibleRange.startIndex; i <= visibleRange.endIndex; i++) {
result.push(
<div
key={i}
style={{
position: 'absolute',
top: i * itemHeight,
height: itemHeight,
width: '100%'
}}
>
{renderItem(items[i], i)}
</div>
);
}
return result;
}, [visibleRange, items, itemHeight, renderItem]);
return (
<div
ref={containerRef}
onScroll={handleScroll}
style={{
height: containerHeight,
overflow: 'auto',
position: 'relative'
}}
>
<div style={{ height: totalHeight, position: 'relative' }}>
{visibleItems}
</div>
</div>
);
}Variable Height Items dan Dynamic Measurement
Tantangan lebih kompleks muncul ketika item memiliki tinggi yang berbeda-beda. Pendekatan ini memerlukan measurement strategy dan caching untuk menjaga performa tetap optimal.
interface MeasuredItem {
index: number;
height: number;
offset: number;
}
class DynamicSizeCache {
private measuredItems: Map<number, MeasuredItem> = new Map();
private estimatedItemHeight: number;
private totalMeasuredHeight: number = 0;
private measuredCount: number = 0;
constructor(estimatedItemHeight: number) {
this.estimatedItemHeight = estimatedItemHeight;
}
setMeasurement(index: number, height: number): void {
const existing = this.measuredItems.get(index);
if (existing) {
this.totalMeasuredHeight -= existing.height;
} else {
this.measuredCount++;
}
this.totalMeasuredHeight += height;
this.measuredItems.set(index, {
index,
height,
offset: this.calculateOffset(index)
});
}
getEstimatedHeight(totalItems: number): number {
if (this.measuredCount === 0) {
return totalItems * this.estimatedItemHeight;
}
const averageHeight = this.totalMeasuredHeight / this.measuredCount;
const unmeasuredCount = totalItems - this.measuredCount;
return this.totalMeasuredHeight + unmeasuredCount * averageHeight;
}
private calculateOffset(index: number): number {
let offset = 0;
for (let i = 0; i < index; i++) {
const item = this.measuredItems.get(i);
offset += item ? item.height : this.estimatedItemHeight;
}
return offset;
}
getItemOffset(index: number): number {
const item = this.measuredItems.get(index);
return item ? item.offset : this.calculateOffset(index);
}
findIndexAtOffset(offset: number, totalItems: number): number {
// Binary search untuk efisiensi
let low = 0;
let high = totalItems - 1;
while (low <= high) {
const mid = Math.floor((low + high) / 2);
const midOffset = this.getItemOffset(mid);
if (midOffset === offset) {
return mid;
} else if (midOffset < offset) {
low = mid + 1;
} else {
high = mid - 1;
}
}
return Math.max(0, low - 1);
}
}Optimasi dengan Intersection Observer API
Intersection Observer API menyediakan cara efisien untuk mendeteksi visibility element tanpa polling atau event listener yang mahal. Teknik ini sangat berguna untuk lazy loading images dan deferred rendering.
class LazyLoadManager {
private observer: IntersectionObserver;
private loadCallbacks: Map<Element, () => void> = new Map();
constructor(rootMargin: string = '100px') {
this.observer = new IntersectionObserver(
(entries) => this.handleIntersection(entries),
{
rootMargin,
threshold: 0
}
);
}
private handleIntersection(entries: IntersectionObserverEntry[]): void {
entries.forEach(entry => {
if (entry.isIntersecting) {
const callback = this.loadCallbacks.get(entry.target);
if (callback) {
callback();
this.observer.unobserve(entry.target);
this.loadCallbacks.delete(entry.target);
}
}
});
}
observe(element: Element, onLoad: () => void): void {
this.loadCallbacks.set(element, onLoad);
this.observer.observe(element);
}
disconnect(): void {
this.observer.disconnect();
this.loadCallbacks.clear();
}
}
// Penggunaan dalam React component
function LazyListItem({
data,
onVisible
}: {
data: ListItem;
onVisible: () => void;
}): React.ReactElement {
const ref = useRef<HTMLDivElement>(null);
useEffect(() => {
const manager = new LazyLoadManager('50px');
if (ref.current) {
manager.observe(ref.current, onVisible);
}
return () => manager.disconnect();
}, [onVisible]);
return (
<div ref={ref} className="lazy-item">
{/* Content rendered after visible */}
</div>
);
}Web Workers untuk Heavy Computation
Ketika list memerlukan sorting, filtering, atau transformasi data yang kompleks, Web Workers dapat memindahkan beban komputasi dari main thread untuk menjaga UI tetap responsive.
interface WorkerMessage {
type: 'sort' | 'filter' | 'transform';
data: unknown[];
options?: Record<string, unknown>;
}
interface WorkerResponse {
type: string;
result: unknown[];
duration: number;
}
self.onmessage = (event: MessageEvent<WorkerMessage>) => {
const { type, data, options } = event.data;
const startTime = performance.now();
let result: unknown[];
switch (type) {
case 'sort':
result = sortData(data, options);
break;
case 'filter':
result = filterData(data, options);
break;
case 'transform':
result = transformData(data, options);
break;
default:
result = data;
}
const response: WorkerResponse = {
type,
result,
duration: performance.now() - startTime
};
self.postMessage(response);
};
function sortData(
data: unknown[],
options?: Record<string, unknown>
): unknown[] {
const key = options?.sortKey as string || 'id';
const direction = options?.direction as 'asc' | 'desc' || 'asc';
return [...data].sort((a: Record<string, unknown>, b: Record<string, unknown>) => {
const aVal = a[key];
const bVal = b[key];
const comparison = aVal < bVal ? -1 : aVal > bVal ? 1 : 0;
return direction === 'asc' ? comparison : -comparison;
});
}
function filterData(
data: unknown[],
options?: Record<string, unknown>
): unknown[] {
const predicate = options?.predicate as (item: unknown) => boolean;
return predicate ? data.filter(predicate) : data;
}
function transformData(
data: unknown[],
options?: Record<string, unknown>
): unknown[] {
const transformer = options?.transform as (item: unknown) => unknown;
return transformer ? data.map(transformer) : data;
}class ListWorkerManager {
private worker: Worker;
private pendingTasks: Map<string, {
resolve: (value: unknown[]) => void;
reject: (reason: Error) => void;
}> = new Map();
constructor() {
this.worker = new Worker(
new URL('./worker.ts', import.meta.url)
);
this.worker.onmessage = this.handleMessage.bind(this);
}
private handleMessage(event: MessageEvent<WorkerResponse>): void {
const { type, result } = event.data;
const task = this.pendingTasks.get(type);
if (task) {
task.resolve(result);
this.pendingTasks.delete(type);
}
}
async sortData<T>(
data: T[],
sortKey: keyof T,
direction: 'asc' | 'desc' = 'asc'
): Promise<T[]> {
return this.executeTask('sort', data, { sortKey, direction }) as Promise<T[]>;
}
private executeTask(
type: string,
data: unknown[],
options?: Record<string, unknown>
): Promise<unknown[]> {
return new Promise((resolve, reject) => {
this.pendingTasks.set(type, { resolve, reject });
this.worker.postMessage({ type, data, options });
});
}
terminate(): void {
this.worker.terminate();
}
}Memory Management dan Garbage Collection
Pada aplikasi dengan list yang sangat besar, memory management menjadi crucial. Teknik object pooling dan WeakMap dapat membantu mengurangi tekanan pada garbage collector.
class ObjectPool<T> {
private pool: T[] = [];
private factory: () => T;
private reset: (obj: T) => void;
private maxSize: number;
constructor(
factory: () => T,
reset: (obj: T) => void,
maxSize: number = 100
) {
this.factory = factory;
this.reset = reset;
this.maxSize = maxSize;
}
acquire(): T {
if (this.pool.length > 0) {
return this.pool.pop()!;
}
return this.factory();
}
release(obj: T): void {
if (this.pool.length < this.maxSize) {
this.reset(obj);
this.pool.push(obj);
}
}
clear(): void {
this.pool = [];
}
get size(): number {
return this.pool.length;
}
}
// Penggunaan untuk DOM elements
const elementPool = new ObjectPool<HTMLDivElement>(
() => document.createElement('div'),
(el) => {
el.className = '';
el.innerHTML = '';
el.style.cssText = '';
},
50
);Pertanyaan Interview Terkait ListView Performance
Dalam technical interview untuk posisi senior frontend developer, pemahaman mendalam tentang virtualisasi list sering menjadi topik utama. Berikut adalah pertanyaan-pertanyaan umum yang sering muncul.
Pertanyaan pertama yang sering diajukan adalah mengapa virtualisasi list diperlukan dan kapan sebaiknya digunakan. Jawaban yang diharapkan mencakup pemahaman tentang DOM overhead, memory consumption, dan threshold jumlah item yang memerlukan virtualisasi (biasanya lebih dari 100-500 item tergantung kompleksitas).
Pertanyaan kedua mengenai bagaimana menangani item dengan variable height dalam virtual list. Interviewer mengharapkan jawaban tentang estimation techniques, measurement caching, binary search untuk offset calculation, dan trade-offs antara accuracy dan performance.
Pertanyaan ketiga tentang optimasi yang dapat dilakukan untuk mengurangi re-render pada virtual list. Jawaban harus mencakup memoization strategies, stable keys, windowing calculations outside render cycle, dan penggunaan CSS transforms versus top/left positioning.
Siap menguasai wawancara Node.js / NestJS Anda?
Berlatih dengan simulator interaktif, flashcards, dan tes teknis kami.
Kesimpulan
Virtualisasi list merupakan teknik essential dalam pengembangan aplikasi JavaScript modern yang menangani dataset besar. Pemahaman mendalam tentang windowing, lazy loading, dan memory management tidak hanya meningkatkan performa aplikasi tetapi juga menjadi nilai tambah signifikan dalam technical interview. Dengan menerapkan teknik-teknik yang dibahas dalam artikel ini, developer dapat membangun list view yang mampu menangani jutaan item dengan performa 60 FPS yang konsisten.
Bisakah kamu menemukan bug di Node.js / NestJS?
Satu potongan kode nyata, satu bug tersembunyi, satu percobaan per hari. Tanpa akun untuk mencoba.

Ditulis oleh
Anthony Fillion-MailletPendiri SharpSkill
Developer fullstack selama lebih dari 10 tahun. Ia menjalankan SharpSkill dan bertanggung jawab atas semua yang diterbitkan di sini.
Diperbarui 7 September 2026
Tag
Bagikan
Artikel terkait

Fast JavaScript ListView 2026: Teknik Windowing, Virtualisasi dan Optimisasi Performa
Kuasai teknik virtualisasi list JavaScript dengan TanStack Virtual, react-virtuoso dan react-window. Pelajari windowing untuk merender 100.000+ item pada 60fps dengan contoh kode lengkap.

Pertanyaan Wawancara Backend Node.js: Panduan Lengkap 2026
25 pertanyaan wawancara backend Node.js yang paling sering ditanyakan. Event loop, async/await, streams, clustering, dan performa dijelaskan dengan jawaban terperinci.

NestJS dan Redis di Tahun 2026: Caching, Sessions, dan Pertanyaan Interview
Panduan lengkap integrasi NestJS dengan Redis untuk caching dan session management, dilengkapi pertanyaan interview untuk persiapan wawancara kerja Node.js.