Fast JavaScript ListView 2026: Kỹ Thuật Windowing, Virtualization và Tối Ưu Hiệu Suất
Thành thạo kỹ thuật virtualization danh sách JavaScript với TanStack Virtual, react-virtuoso và react-window. Học windowing để render 100.000+ item ở 60fps với ví dụ code đầy đủ.

Render hàng ngàn item trong JavaScript ListView mà không có virtualization khiến trình duyệt tạo ra hàng ngàn DOM node, dẫn đến thời gian render ban đầu chậm, scrolling bị giật lag, và tiêu thụ bộ nhớ có thể làm crash thiết bị di động. Các triển khai Fast JavaScript ListView trong năm 2026 giải quyết vấn đề này thông qua windowing: chỉ render 10-20 item hiển thị trong viewport trong khi mô phỏng vùng scrollable chứa tất cả item.
Windowing chỉ render các item hiển thị cộng với buffer nhỏ (overscan), giảm DOM node từ 10.000 xuống còn khoảng 20 bất kể kích thước danh sách.
Tại Sao Hiệu Suất Trình Duyệt Giảm Với Danh Sách Lớn
Mỗi DOM node tiêu thụ bộ nhớ và yêu cầu trình duyệt theo dõi vị trí của nó trong quá trình tính toán layout. Một danh sách 10.000 item với markup phức tạp vừa phải (avatar, hai text span, một nút) tạo ra 40.000+ DOM node. Engine render của Chrome tính toán lại style và vị trí cho tất cả chúng trên mỗi sự kiện scroll.
Kết quả: render ban đầu mất 2-4 giây, scrolling giảm xuống 15fps, và sử dụng bộ nhớ tăng lên 500MB+. Trên mobile Safari, tab thường crash ngay lập tức.
Virtualization khắc phục điều này bằng cách duy trì số lượng DOM node không đổi. Dù mảng dữ liệu chứa 100 hay 100.000 item, DOM được render vẫn giữ ở khoảng 20-30 node, giữ bộ nhớ ổn định và hiệu suất scroll ở 60fps.
import { useVirtualizer } from '@tanstack/react-virtual'
import { useRef } from 'react'
interface Item {
id: string
name: string
email: string
}
export function VirtualizedList({ items }: { items: Item[] }) {
const parentRef = useRef<HTMLDivElement>(null)
// Create virtualizer instance with estimated row height
const virtualizer = useVirtualizer({
count: items.length,
getScrollElement: () => parentRef.current,
estimateSize: () => 60, // estimated height in pixels
overscan: 5, // render 5 extra items above/below viewport
})
return (
<div ref={parentRef} style={{ height: '600px', overflow: 'auto' }}>
<div style={{ height: `${virtualizer.getTotalSize()}px`, position: 'relative' }}>
{virtualizer.getVirtualItems().map((virtualRow) => {
const item = items[virtualRow.index]
return (
<div
key={item.id}
style={{
position: 'absolute',
top: 0,
left: 0,
width: '100%',
height: `${virtualRow.size}px`,
transform: `translateY(${virtualRow.start}px)`,
}}
>
<div className="flex items-center gap-3 p-3 border-b">
<div className="w-10 h-10 rounded-full bg-gray-200" />
<div>
<p className="font-medium">{item.name}</p>
<p className="text-sm text-gray-500">{item.email}</p>
</div>
</div>
</div>
)
})}
</div>
</div>
)
}Triển khai này chỉ render các hàng hiển thị cộng với buffer overscan. Container bên ngoài thiết lập tổng chiều cao scrollable, làm cho scrollbar hoạt động như thể tất cả item tồn tại trong DOM.
TanStack Virtual: Phương Pháp Headless Cho 2026
TanStack Virtual cung cấp các primitive virtualization không phụ thuộc framework. Không giống react-window hay react-virtuoso, thư viện này không cung cấp component dựng sẵn, chỉ có hooks tính toán item nào cần render và vị trí của chúng. Kiến trúc headless này cho phép kiểm soát hoàn toàn styling và cấu trúc DOM.
Phiên bản hiện tại (3.13) hỗ trợ React, Vue, Solid, Svelte, và vanilla JavaScript. Các tùy chọn cấu hình chính:
| Tùy chọn | Mục đích | Mặc định |
|---|---|---|
count | Tổng số item | bắt buộc |
estimateSize | Hàm trả về chiều cao/rộng ước tính của item | bắt buộc |
overscan | Item bổ sung được render ngoài viewport | 1 |
getScrollElement | Trả về container scrollable | bắt buộc |
measureElement | Hàm đo lường tùy chỉnh cho kích thước động | auto |
Giá trị overscan ảnh hưởng trực tiếp đến độ mượt của scroll. Đặt quá thấp (0-2) gây ra flash trắng khi scroll nhanh. Đặt quá cao (20+) phủ nhận lợi ích của virtualization. Với hầu hết ứng dụng, 5-10 cung cấp sự cân bằng tối ưu.
import { useVirtualizer } from '@tanstack/react-virtual'
import { useRef, useCallback } from 'react'
interface Message {
id: string
content: string
timestamp: number
}
export function ChatList({ messages }: { messages: Message[] }) {
const parentRef = useRef<HTMLDivElement>(null)
// measureElement enables dynamic height measurement
const virtualizer = useVirtualizer({
count: messages.length,
getScrollElement: () => parentRef.current,
estimateSize: () => 80, // rough estimate, actual measured later
overscan: 8,
})
return (
<div ref={parentRef} style={{ height: '100vh', overflow: 'auto' }}>
<div
style={{
height: `${virtualizer.getTotalSize()}px`,
width: '100%',
position: 'relative',
}}
>
{virtualizer.getVirtualItems().map((virtualRow) => {
const message = messages[virtualRow.index]
return (
<div
key={message.id}
data-index={virtualRow.index}
ref={virtualizer.measureElement} // enables auto-measurement
style={{
position: 'absolute',
top: 0,
left: 0,
width: '100%',
transform: `translateY(${virtualRow.start}px)`,
}}
>
<div className="p-4 border-b">
<p className="text-sm text-gray-500">
{new Date(message.timestamp).toLocaleTimeString()}
</p>
<p className="mt-1">{message.content}</p>
</div>
</div>
)
})}
</div>
</div>
)
}Ref measureElement cho phép đo chiều cao tự động sau khi render. Điều này xử lý độ dài nội dung thay đổi mà không cần tính toán chiều cao thủ công.
react-virtuoso: Giải Pháp Thay Thế Giàu Tính Năng
react-virtuoso có cách tiếp cận ngược lại: thư viện component batteries-included. Trong khi TanStack Virtual yêu cầu xây dựng scroll container và logic positioning, react-virtuoso xử lý những điều này bên trong.
Đánh đổi là kích thước bundle (37KB so với 12KB của TanStack Virtual) nhưng ít boilerplate hơn đáng kể cho các pattern phổ biến như grouped list, sticky header, và giao diện chat với reverse scrolling.
import { Virtuoso } from 'react-virtuoso'
interface User {
id: string
name: string
department: string
}
export function UserDirectory({ users }: { users: User[] }) {
return (
<Virtuoso
style={{ height: '600px' }}
data={users}
itemContent={(index, user) => (
<div className="flex items-center gap-3 p-4 border-b hover:bg-gray-50">
<div className="w-12 h-12 rounded-full bg-blue-100 flex items-center justify-center">
{user.name.charAt(0)}
</div>
<div>
<p className="font-medium">{user.name}</p>
<p className="text-sm text-gray-500">{user.department}</p>
</div>
</div>
)}
/>
)
}Điều này đạt được kết quả virtualization tương tự với ít code hơn. react-virtuoso tự động đo item, xử lý chiều cao động, và cung cấp persistence vị trí scroll.
Để chuẩn bị phỏng vấn về hiệu suất React, hiểu khi nào chọn mỗi thư viện thể hiện nhận thức kiến trúc. TanStack Virtual phù hợp với ứng dụng đã sử dụng TanStack Table hoặc yêu cầu code không phụ thuộc framework. react-virtuoso phù hợp với các team ưu tiên tốc độ phát triển hơn kích thước bundle.
Sẵn sàng chinh phục phỏng vấn Node.js / NestJS?
Luyện tập với mô phỏng tương tác, flashcards và bài kiểm tra kỹ thuật.
Các Lỗi Thường Gặp và Giải Pháp
Vị Trí Scroll Nhảy Với Hình Ảnh
Hình ảnh tải bất đồng bộ, thay đổi chiều cao hàng sau khi đo lường ban đầu. Virtualizer không thể dự đoán thay đổi này, gây ra nội dung bên dưới bị dịch chuyển.
Cách khắc phục: đặt trước không gian cho hình ảnh sử dụng aspect-ratio hoặc kích thước cố định.
interface ImageItem {
id: string
src: string
title: string
}
export function ImageListItem({ item }: { item: ImageItem }) {
return (
<div className="flex gap-4 p-4 border-b">
{/* Reserve 80x80px space regardless of load state */}
<div className="w-20 h-20 flex-shrink-0 bg-gray-100">
<img
src={item.src}
alt={item.title}
className="w-full h-full object-cover"
loading="lazy"
/>
</div>
<p className="font-medium">{item.title}</p>
</div>
)
}Mất State Khi Scroll
Các item được virtualize unmount khi scroll ra khỏi vùng nhìn thấy. Bất kỳ state cục bộ nào (input form, accordion mở rộng) đều biến mất.
Giải pháp: nâng state lên component cha hoặc thư viện quản lý state, key theo ID của item.
import { useState, useCallback } from 'react'
import { useVirtualizer } from '@tanstack/react-virtual'
interface FAQ {
id: string
question: string
answer: string
}
export function FAQList({ faqs }: { faqs: FAQ[] }) {
// State lives in parent, survives row unmounting
const [expandedIds, setExpandedIds] = useState<Set<string>>(new Set())
const parentRef = useRef<HTMLDivElement>(null)
const toggleExpanded = useCallback((id: string) => {
setExpandedIds(prev => {
const next = new Set(prev)
if (next.has(id)) {
next.delete(id)
} else {
next.add(id)
}
return next
})
}, [])
const virtualizer = useVirtualizer({
count: faqs.length,
getScrollElement: () => parentRef.current,
estimateSize: () => 60,
overscan: 5,
})
return (
<div ref={parentRef} style={{ height: '600px', overflow: 'auto' }}>
<div style={{ height: `${virtualizer.getTotalSize()}px`, position: 'relative' }}>
{virtualizer.getVirtualItems().map((virtualRow) => {
const faq = faqs[virtualRow.index]
const isExpanded = expandedIds.has(faq.id)
return (
<div
key={faq.id}
data-index={virtualRow.index}
ref={virtualizer.measureElement}
style={{
position: 'absolute',
top: 0,
left: 0,
width: '100%',
transform: `translateY(${virtualRow.start}px)`,
}}
>
<button
onClick={() => toggleExpanded(faq.id)}
className="w-full text-left p-4 border-b"
>
<p className="font-medium">{faq.question}</p>
{isExpanded && (
<p className="mt-2 text-gray-600">{faq.answer}</p>
)}
</button>
</div>
)
})}
</div>
</div>
)
}Key Không Ổn Định Gây Re-render
Sử dụng chỉ số mảng làm key hoạt động cho đến khi danh sách được sắp xếp lại. Item nhận state sai, gây ra glitch visual và render lãng phí.
Luôn sử dụng định danh duy nhất, ổn định từ chính dữ liệu:
// Correct: stable key from data
{virtualizer.getVirtualItems().map((virtualRow) => {
const item = items[virtualRow.index]
return <div key={item.id}>...</div> // id from database/API
})}
// Wrong: index changes when list reorders
{virtualizer.getVirtualItems().map((virtualRow, index) => {
return <div key={index}>...</div> // causes bugs on reorder
})}So Sánh Thư Viện Cho Dự Án 2026
Chọn thư viện virtualization phù hợp phụ thuộc vào yêu cầu dự án. So sánh này phản ánh hệ sinh thái 2026:
| Tiêu chí | TanStack Virtual | react-virtuoso | react-window |
|---|---|---|---|
| Kích thước bundle | 12KB | 37KB | 6KB |
| Chiều cao động | Cài đặt thủ công | Tự động | Không hỗ trợ |
| TypeScript | Native | Native | package @types |
| Hỗ trợ framework | React, Vue, Solid, Svelte | Chỉ React | Chỉ React |
| Bảo trì | Tích cực (2026) | Tích cực (2026) | Chế độ bảo trì |
| Độ khó học | Cao hơn | Thấp hơn | Thấp nhất |
react-window vẫn khả thi cho danh sách fixed-height đơn giản khi kích thước bundle quan trọng. Tuy nhiên, trạng thái maintenance-mode và thiếu hỗ trợ chiều cao động khiến nó không phù hợp cho dự án mới với yêu cầu phức tạp.
Để xây dựng ứng dụng React hiệu suất cao, hiểu các đánh đổi virtualization thể hiện nhận thức về hiệu suất mà các interviewer tìm kiếm.
Đo Lường Cải Thiện Hiệu Suất
Định lượng lợi ích của virtualization đòi hỏi phương pháp đo lường nhất quán. Panel Performance của Chrome DevTools cung cấp các metric liên quan.
Các phép đo chính trước/sau virtualization:
- First Contentful Paint (FCP): Thời gian cho đến khi nội dung đầu tiên được render. Virtualization giảm điều này bằng cách hạn chế việc tạo DOM ban đầu.
- Total Blocking Time (TBT): Blocking main thread trong quá trình load. Ít DOM node hơn nghĩa là ít tính toán style/layout hơn.
- Sử dụng bộ nhớ: Snapshot heap size trong panel Memory. Nên giữ không đổi bất kể độ dài danh sách.
- Scroll fps: Ghi lại trace Performance khi scrolling. Frame rate nên giữ ở 60fps.
import { useEffect, useRef } from 'react'
export function useScrollFPS(containerRef: React.RefObject<HTMLElement>) {
const frameCountRef = useRef(0)
const lastTimeRef = useRef(performance.now())
useEffect(() => {
const container = containerRef.current
if (!container) return
let rafId: number
const measureFrame = () => {
frameCountRef.current++
const now = performance.now()
if (now - lastTimeRef.current >= 1000) {
console.log(`Scroll FPS: ${frameCountRef.current}`)
frameCountRef.current = 0
lastTimeRef.current = now
}
rafId = requestAnimationFrame(measureFrame)
}
const handleScroll = () => {
if (!rafId) {
rafId = requestAnimationFrame(measureFrame)
}
}
container.addEventListener('scroll', handleScroll, { passive: true })
return () => {
container.removeEventListener('scroll', handleScroll)
cancelAnimationFrame(rafId)
}
}, [containerRef])
}Hook này ghi log scroll FPS trong quá trình development. Ứng dụng production nên sử dụng công cụ observability như Sentry Performance hoặc telemetry tùy chỉnh.
Bắt đầu luyện tập!
Kiểm tra kiến thức với mô phỏng phỏng vấn và bài kiểm tra kỹ thuật.
Những Điểm Chính Về Hiệu Suất Danh Sách JavaScript
- Virtualization giảm DOM node từ hàng ngàn xuống còn khoảng 20, giữ bộ nhớ và hiệu suất scroll không đổi bất kể kích thước dữ liệu
- TanStack Virtual (12KB) cung cấp primitive headless cho kiểm soát tối đa trên React, Vue, Solid, và Svelte
- react-virtuoso (37KB) cung cấp component batteries-included với xử lý chiều cao động tự động
- react-window vẫn trong chế độ maintenance từ 2022 và thiếu hỗ trợ chiều cao động
- Đặt overscan từ 5-10 để có độ mượt scroll tối ưu mà không phủ nhận lợi ích virtualization
- Đặt trước kích thước hình ảnh để ngăn nhảy vị trí scroll trong quá trình tải bất đồng bộ
- Nâng state expandable/editable lên component cha vì các hàng virtualized unmount khi scroll
- Sử dụng key ổn định từ ID dữ liệu, không bao giờ dùng chỉ số mảng, để ngăn không khớp state khi sắp xếp lại
- Đo FCP, TBT, bộ nhớ, và scroll FPS trước/sau triển khai để định lượng cải thiện
Bạn có tìm ra lỗi trong Node.js / NestJS không?
Một đoạn mã thật, một lỗi ẩn, mỗi ngày một lượt. Không cần tài khoản để thử.

Viết bởi
Anthony Fillion-MailletNgười sáng lập SharpSkill
Lập trình viên fullstack hơn 10 năm. Anh điều hành SharpSkill và chịu trách nhiệm về mọi nội dung đăng tại đây.
Cập nhật ngày 15 tháng 9, 2026
Thẻ
Chia sẻ
Bài viết liên quan

High Performance JavaScript ListView 2026: Kỹ Thuật Ảo Hóa và Tối Ưu Hóa
Hướng dẫn toàn diện về kỹ thuật ảo hóa danh sách JavaScript để đạt hiệu năng cao trong năm 2026. Tìm hiểu windowing, lazy loading và tối ưu hóa rendering cho ứng dụng quy mô lớn.

Microservices với NestJS năm 2026: Kiến trúc, gRPC và Câu hỏi Phỏng vấn
Hướng dẫn toàn diện về kiến trúc microservices NestJS với gRPC: transport layer, Protocol Buffers, streaming patterns và câu hỏi phỏng vấn cho backend engineer năm 2026.

NestJS + Prisma: stack backend hiện đại cho Node.js
Hướng dẫn đầy đủ để xây dựng API backend hiện đại với NestJS và Prisma. Cấu hình, model, service, transaction và các best practice được giải thích chi tiết.