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 đủ.

Virtualization danh sách JavaScript với kỹ thuật windowing để tối ưu hiệu suất scrolling

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 Trong Một Câu

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.

VirtualizedList.tsxtypescript
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ọnMục đíchMặc định
countTổng số itembắt buộc
estimateSizeHàm trả về chiều cao/rộng ước tính của itembắt buộc
overscanItem bổ sung được render ngoài viewport1
getScrollElementTrả về container scrollablebắt buộc
measureElementHàm đo lường tùy chỉnh cho kích thước độngauto

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.

DynamicHeightList.tsxtypescript
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.

VirtuosoList.tsxtypescript
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.

ImageListItem.tsxtypescript
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.

ExpandableList.tsxtypescript
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:

typescript
// 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 Virtualreact-virtuosoreact-window
Kích thước bundle12KB37KB6KB
Chiều cao độngCài đặt thủ côngTự độngKhông hỗ trợ
TypeScriptNativeNativepackage @types
Hỗ trợ frameworkReact, Vue, Solid, SvelteChỉ ReactChỉ React
Bảo trìTích cực (2026)Tích cực (2026)Chế độ bảo trì
Độ khó họcCao hơnThấp hơnThấ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.
PerformanceMonitor.tsxtypescript
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
Thử thách hôm nay

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ử.

Anthony Fillion-Maillet

Viết bởi

Anthony Fillion-Maillet

Ngườ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ẻ

#javascript
#performance
#virtualization
#react
#typescript

Chia sẻ

Bài viết liên quan