High Performance JavaScript ListView 2026: เทคนิค Virtualization และการเพิ่มประสิทธิภาพ
คู่มือฉบับสมบูรณ์เกี่ยวกับเทคนิค virtualization สำหรับ list ใน JavaScript เพื่อประสิทธิภาพสูงในปี 2026 เรียนรู้ windowing, lazy loading และการเพิ่มประสิทธิภาพ rendering สำหรับแอปพลิเคชันขนาดใหญ่

ประสิทธิภาพของ list view เป็นหนึ่งในความท้าทายที่สำคัญที่สุดในการพัฒนาแอปพลิเคชัน JavaScript สมัยใหม่ เมื่อต้องจัดการกับข้อมูลหลายพันถึงหลายล้านรายการ วิธีการ rendering แบบดั้งเดิมอาจทำให้เกิด bottleneck ที่รุนแรง ส่งผลกระทบต่อความตอบสนองของแอปพลิเคชันและประสบการณ์ผู้ใช้โดยรวม บทความนี้นำเสนอเทคนิค virtualization และการเพิ่มประสิทธิภาพล่าสุดที่ใช้ในการพัฒนา JavaScript ระดับมืออาชีพในปี 2026
Virtualization ของ list สามารถลดการใช้หน่วยความจำได้ถึง 95% และเพิ่ม frame rate จาก 10 FPS เป็น 60 FPS บนชุดข้อมูลขนาดใหญ่ เทคนิคนี้กลายเป็นคำถามบังคับในการสัมภาษณ์ทางเทคนิคสำหรับตำแหน่ง frontend senior ในปี 2026
ทำความเข้าใจปัญหาประสิทธิภาพของ List Rendering
เมื่อแอปพลิเคชัน render list ที่มี 10,000 รายการโดยใช้วิธีการทั่วไป browser จะต้องสร้าง DOM nodes 10,000 อัน แต่ละอันมี event listeners, styles และ layout calculations ของตัวเอง กระบวนการนี้ใช้หน่วยความจำจำนวนมากและทำให้เกิด jank ที่เห็นได้ชัดเมื่อเลื่อนหน้าจอ
ปัญหาหลักอยู่ที่วิธีที่ browser จัดการ DOM operations การเพิ่ม node แต่ละครั้งต้องการ reflow และ repaint ซึ่งจะช้าลงแบบ exponential เมื่อจำนวน element เพิ่มขึ้น บนอุปกรณ์มือถือที่มีทรัพยากรจำกัด ปัญหานี้จะยิ่งเด่นชัดมากขึ้น
// ตัวอย่าง naive approach ที่ทำให้เกิดปัญหาประสิทธิภาพ
interface ListItem {
id: number;
title: string;
description: string;
}
function renderAllItems(items: ListItem[]): void {
const container = document.getElementById('list-container');
// สิ่งนี้จะสร้าง DOM nodes 10,000 อันพร้อมกัน
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);
});
}หลักการพื้นฐานของ Windowing และ Virtual Scrolling
Virtualization ของ list ทำงานบนหลักการที่เรียบง่ายแต่ทรงพลัง: render เฉพาะรายการที่มองเห็นใน viewport บวกกับ buffer เล็กน้อยเพื่อการเลื่อนที่ลื่นไหล เทคนิคนี้เรียกว่า windowing หรือ virtual scrolling
แนวคิดหลักประกอบด้วย visible window ซึ่งเป็นพื้นที่ viewport ที่ผู้ใช้มองเห็น, overscan ซึ่งเป็น buffer เพิ่มเติมด้านบนและด้านล่างของ visible window เพื่อป้องกัน flickering และ item pool ซึ่งเป็น DOM elements ที่ถูกนำมาใช้ซ้ำเพื่อหลีกเลี่ยงการสร้างและลบซ้ำๆ
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;
// คำนวณ index ของรายการแรกที่มองเห็น
const startIndex = Math.max(
0,
Math.floor(scrollTop / itemHeight) - overscan
);
// คำนวณจำนวนรายการที่พอดีใน viewport
const visibleCount = Math.ceil(containerHeight / itemHeight);
// คำนวณ index ของรายการสุดท้ายที่ต้อง render
const endIndex = Math.min(
totalItems - 1,
startIndex + visibleCount + overscan * 2
);
// Offset เพื่อวางตำแหน่งรายการอย่างถูกต้อง
const offsetY = startIndex * itemHeight;
return { startIndex, endIndex, offsetY };
}การ Implement Virtual List ด้วย React
ใน React ecosystem ไลบรารีอย่าง react-window และ react-virtualized ได้กลายเป็นมาตรฐานในอุตสาหกรรม อย่างไรก็ตาม การเข้าใจกลไกภายในของ virtualization มีความสำคัญอย่างยิ่งสำหรับการสัมภาษณ์ทางเทคนิคและการ debug ปัญหาประสิทธิภาพ
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 และ Dynamic Measurement
ความท้าทายที่ซับซ้อนมากขึ้นเกิดขึ้นเมื่อรายการมีความสูงที่แตกต่างกัน วิธีการนี้ต้องการ measurement strategy และ caching เพื่อรักษาประสิทธิภาพให้เหมาะสมที่สุด
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 เพื่อประสิทธิภาพ
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);
}
}การเพิ่มประสิทธิภาพด้วย Intersection Observer API
Intersection Observer API ให้วิธีที่มีประสิทธิภาพในการตรวจจับ visibility ของ element โดยไม่ต้อง polling หรือ event listener ที่มีค่าใช้จ่ายสูง เทคนิคนี้มีประโยชน์มากสำหรับ lazy loading images และ 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();
}
}
// การใช้งานใน 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 ถูก render หลังจากมองเห็น */}
</div>
);
}Web Workers สำหรับ Heavy Computation
เมื่อ list ต้องการ sorting, filtering หรือการแปลงข้อมูลที่ซับซ้อน Web Workers สามารถย้ายภาระการคำนวณออกจาก main thread เพื่อให้ UI ยังคง 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();
}
}การจัดการหน่วยความจำและ Garbage Collection
ในแอปพลิเคชันที่มี list ขนาดใหญ่มาก การจัดการหน่วยความจำกลายเป็นสิ่งสำคัญ เทคนิค object pooling และ WeakMap สามารถช่วยลดแรงกดดันบน 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;
}
}
// การใช้งานสำหรับ DOM elements
const elementPool = new ObjectPool<HTMLDivElement>(
() => document.createElement('div'),
(el) => {
el.className = '';
el.innerHTML = '';
el.style.cssText = '';
},
50
);คำถามสัมภาษณ์เกี่ยวกับ ListView Performance
ในการสัมภาษณ์ทางเทคนิคสำหรับตำแหน่ง senior frontend developer ความเข้าใจอย่างลึกซึ้งเกี่ยวกับ virtualization ของ list มักเป็นหัวข้อหลัก ต่อไปนี้คือคำถามทั่วไปที่พบบ่อย
คำถามแรกที่มักถูกถามคือทำไมต้องใช้ virtualization สำหรับ list และควรใช้เมื่อใด คำตอบที่คาดหวังรวมถึงความเข้าใจเกี่ยวกับ DOM overhead, memory consumption และ threshold ของจำนวนรายการที่ต้องการ virtualization (โดยทั่วไปมากกว่า 100-500 รายการขึ้นอยู่กับความซับซ้อน)
คำถามที่สองเกี่ยวกับวิธีจัดการรายการที่มีความสูงแปรผันใน virtual list ผู้สัมภาษณ์คาดหวังคำตอบเกี่ยวกับ estimation techniques, measurement caching, binary search สำหรับ offset calculation และ trade-offs ระหว่าง accuracy และ performance
คำถามที่สามเกี่ยวกับการเพิ่มประสิทธิภาพที่สามารถทำได้เพื่อลด re-render บน virtual list คำตอบควรรวมถึง memoization strategies, stable keys, windowing calculations นอก render cycle และการใช้ CSS transforms แทน top/left positioning
พร้อมที่จะพิชิตการสัมภาษณ์ Node.js / NestJS แล้วหรือยังครับ?
ฝึกฝนด้วยตัวจำลองแบบโต้ตอบ, flashcards และแบบทดสอบเทคนิคครับ
สรุป
Virtualization ของ list เป็นเทคนิคที่จำเป็นในการพัฒนาแอปพลิเคชัน JavaScript สมัยใหม่ที่จัดการกับชุดข้อมูลขนาดใหญ่ ความเข้าใจอย่างลึกซึ้งเกี่ยวกับ windowing, lazy loading และการจัดการหน่วยความจำไม่เพียงแต่ปรับปรุงประสิทธิภาพของแอปพลิเคชันเท่านั้น แต่ยังกลายเป็นคุณค่าเพิ่มที่สำคัญในการสัมภาษณ์ทางเทคนิค ด้วยการนำเทคนิคที่กล่าวถึงในบทความนี้ไปใช้ นักพัฒนาสามารถสร้าง list view ที่สามารถจัดการรายการหลายล้านรายการด้วยประสิทธิภาพ 60 FPS อย่างสม่ำเสมอ
คุณหาบั๊กใน Node.js / NestJS เจอไหม
โค้ดจริงหนึ่งชิ้น บั๊กที่ซ่อนอยู่หนึ่งจุด วันละหนึ่งครั้ง ลองได้โดยไม่ต้องมีบัญชี

เขียนโดย
Anthony Fillion-Mailletผู้ก่อตั้ง SharpSkill
เป็นนักพัฒนาฟูลสแตกมากว่า 10 ปี ดูแล SharpSkill และรับผิดชอบทุกสิ่งที่เผยแพร่ที่นี่
อัปเดตเมื่อ 7 กันยายน 2569
แท็ก
แชร์
บทความที่เกี่ยวข้อง

Fast JavaScript ListView 2026: เทคนิค Windowing, Virtualization และการเพิ่มประสิทธิภาพ
เชี่ยวชาญเทคนิค virtualization รายการ JavaScript ด้วย TanStack Virtual, react-virtuoso และ react-window เรียนรู้ windowing เพื่อ render 100,000+ รายการที่ 60fps พร้อมตัวอย่างโค้ด

คำถามสัมภาษณ์ Backend Node.js: คู่มือฉบับสมบูรณ์ 2026
25 คำถามสัมภาษณ์ Backend Node.js ที่พบบ่อยที่สุด Event loop, async/await, streams, clustering และประสิทธิภาพอธิบายพร้อมคำตอบโดยละเอียด

NestJS และ Redis ในปี 2026: Caching, Sessions และคำถามสัมภาษณ์
คู่มือฉบับสมบูรณ์สำหรับการผสาน NestJS กับ Redis สำหรับ caching และการจัดการ session พร้อมคำถามสัมภาษณ์เพื่อเตรียมตัวสำหรับการสัมภาษณ์งาน Node.js