Fast JavaScript ListView 2026: เทคนิค Windowing, Virtualization และการเพิ่มประสิทธิภาพ

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

Virtualization รายการ JavaScript ด้วยเทคนิค windowing เพื่อประสิทธิภาพการ scroll ที่รวดเร็ว

การ render รายการหลายพันรายการใน JavaScript ListView โดยไม่มี virtualization ทำให้เบราว์เซอร์สร้าง DOM node หลายพัน node ส่งผลให้เวลา render เริ่มต้นช้า การ scroll กระตุก และการใช้หน่วยความจำที่อาจทำให้อุปกรณ์มือถือค้าง การใช้งาน Fast JavaScript ListView ในปี 2026 แก้ปัญหานี้ผ่าน windowing: render เฉพาะ 10-20 รายการที่มองเห็นใน viewport ในขณะที่จำลองพื้นที่ scrollable ที่มีรายการทั้งหมด

Windowing ในหนึ่งประโยค

Windowing render เฉพาะรายการที่มองเห็นบวกกับ buffer เล็กๆ (overscan) ลด DOM node จาก 10,000 เหลือประมาณ 20 โดยไม่คำนึงถึงขนาดของรายการ

ทำไมประสิทธิภาพเบราว์เซอร์ลดลงเมื่อมีรายการจำนวนมาก

ทุก DOM node ใช้หน่วยความจำและต้องการให้เบราว์เซอร์ติดตามตำแหน่งของมันระหว่างการคำนวณ layout รายการ 10,000 รายการที่มี markup ซับซ้อนปานกลาง (avatar, text span สองอัน, ปุ่มหนึ่งปุ่ม) สร้าง DOM node มากกว่า 40,000 node เอนจิน rendering ของ Chrome คำนวณ style และตำแหน่งใหม่สำหรับทุก node ในทุก event การ scroll

ผลลัพธ์: render เริ่มต้นใช้เวลา 2-4 วินาที การ scroll ลดลงเหลือ 15fps และการใช้หน่วยความจำเพิ่มขึ้นถึง 500MB+ บน mobile Safari แท็บมักจะ crash ทันที

Virtualization แก้ไขปัญหานี้โดยการรักษาจำนวน DOM node ให้คงที่ ไม่ว่า array ข้อมูลจะมี 100 หรือ 100,000 รายการ DOM ที่ render จะอยู่ที่ประมาณ 20-30 node รักษาหน่วยความจำให้คงที่และประสิทธิภาพ 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>
  )
}

การใช้งานนี้ render เฉพาะแถวที่มองเห็นบวกกับ buffer overscan container ด้านนอกกำหนดความสูง scrollable ทั้งหมด ทำให้ scrollbar ทำงานเหมือนว่ารายการทั้งหมดมีอยู่ใน DOM

TanStack Virtual: แนวทาง Headless สำหรับปี 2026

TanStack Virtual ให้ primitive virtualization ที่ไม่ขึ้นกับ framework ต่างจาก react-window หรือ react-virtuoso ไลบรารีนี้ไม่ให้ component สำเร็จรูป มีแค่ hooks ที่คำนวณว่ารายการไหนควร render และตำแหน่งของมัน สถาปัตยกรรม headless นี้หมายถึงการควบคุม styling และโครงสร้าง DOM อย่างสมบูรณ์

เวอร์ชันปัจจุบัน (3.13) รองรับ React, Vue, Solid, Svelte และ vanilla JavaScript ตัวเลือกการกำหนดค่าหลัก:

ตัวเลือกวัตถุประสงค์ค่าเริ่มต้น
countจำนวนรายการทั้งหมดrequired
estimateSizeฟังก์ชันที่คืนค่าความสูง/กว้างโดยประมาณของรายการrequired
overscanรายการเพิ่มเติมที่ render นอก viewport1
getScrollElementคืนค่า container ที่ scrollablerequired
measureElementฟังก์ชันการวัดแบบกำหนดเองสำหรับขนาดแบบไดนามิกauto

ค่า overscan ส่งผลโดยตรงต่อความลื่นของการ scroll การตั้งค่าต่ำเกินไป (0-2) ทำให้เกิด flash สีขาวระหว่างการ scroll เร็ว การตั้งค่าสูงเกินไป (20+) ทำให้ประโยชน์ของ virtualization หายไป สำหรับแอปพลิเคชันส่วนใหญ่ 5-10 ให้ความสมดุลที่เหมาะสม

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 ทำให้สามารถวัดความสูงอัตโนมัติหลังจาก render ซึ่งจัดการกับความยาวเนื้อหาที่แตกต่างกันโดยไม่ต้องคำนวณความสูงด้วยตนเอง

react-virtuoso: ทางเลือกที่มีฟีเจอร์ครบครัน

react-virtuoso ใช้แนวทางตรงข้าม: ไลบรารี component แบบ batteries-included ในขณะที่ TanStack Virtual ต้องการสร้าง scroll container และ logic การจัดตำแหน่ง react-virtuoso จัดการสิ่งเหล่านี้ภายใน

การแลกเปลี่ยนคือขนาด bundle (37KB เทียบกับ 12KB สำหรับ TanStack Virtual) แต่ boilerplate น้อยกว่ามากสำหรับ pattern ทั่วไปเช่น grouped list, sticky header และ interface chat ที่มี 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>
      )}
    />
  )
}

นี่ให้ผลลัพธ์ virtualization เดียวกันด้วยโค้ดที่น้อยกว่า react-virtuoso วัดรายการอัตโนมัติ จัดการความสูงแบบไดนามิก และให้ persistence ตำแหน่ง scroll

สำหรับการเตรียมตัวสัมภาษณ์เกี่ยวกับประสิทธิภาพ React การเข้าใจว่าเมื่อไหร่ควรเลือกแต่ละไลบรารีแสดงให้เห็นความตระหนักด้านสถาปัตยกรรม TanStack Virtual เหมาะกับแอปพลิเคชันที่ใช้ TanStack Table อยู่แล้วหรือต้องการโค้ดที่ไม่ขึ้นกับ framework react-virtuoso เหมาะกับทีมที่ให้ความสำคัญกับความเร็วในการพัฒนามากกว่าขนาด bundle

พร้อมที่จะพิชิตการสัมภาษณ์ Node.js / NestJS แล้วหรือยังครับ?

ฝึกฝนด้วยตัวจำลองแบบโต้ตอบ, flashcards และแบบทดสอบเทคนิคครับ

ปัญหาที่พบบ่อยและวิธีแก้ไข

ตำแหน่ง Scroll กระโดดเมื่อมีรูปภาพ

รูปภาพโหลดแบบ asynchronous เปลี่ยนความสูงแถวหลังจากการวัดครั้งแรก Virtualizer ไม่สามารถทำนายการเปลี่ยนแปลงนี้ได้ ทำให้เนื้อหาด้านล่างเลื่อน

วิธีแก้ไข: จองพื้นที่สำหรับรูปภาพโดยใช้ aspect-ratio หรือขนาดคงที่

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>
  )
}

State หายไประหว่าง Scroll

รายการที่ virtualize unmount เมื่อ scroll ออกจากมุมมอง state local ใดๆ (input ฟอร์ม, accordion ที่ขยาย) หายไป

วิธีแก้ไข: ยก state ขึ้นไปที่ component parent หรือไลบรารี state management โดย key ตาม ID ของรายการ

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 ไม่คงที่ทำให้เกิด Re-render

การใช้ index ของ array เป็น key ใช้ได้จนกว่ารายการจะเรียงลำดับใหม่ รายการได้รับ state ผิด ทำให้เกิด glitch visual และ render ที่สูญเปล่า

ใช้ identifier ที่ไม่ซ้ำและคงที่จากข้อมูลเองเสมอ:

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
})}

การเปรียบเทียบไลบรารีสำหรับโปรเจกต์ 2026

การเลือกไลบรารี virtualization ที่เหมาะสมขึ้นอยู่กับความต้องการของโปรเจกต์ การเปรียบเทียบนี้สะท้อน ecosystem ปี 2026:

เกณฑ์TanStack Virtualreact-virtuosoreact-window
ขนาด bundle12KB37KB6KB
ความสูงแบบไดนามิกตั้งค่าด้วยตนเองอัตโนมัติไม่รองรับ
TypeScriptNativeNativepackage @types
รองรับ frameworkReact, Vue, Solid, SvelteReact เท่านั้นReact เท่านั้น
การบำรุงรักษาActive (2026)Active (2026)Maintenance mode
ความยากในการเรียนรู้สูงกว่าต่ำกว่าต่ำสุด

react-window ยังคงใช้ได้สำหรับรายการ fixed-height อย่างง่ายที่ขนาด bundle สำคัญ อย่างไรก็ตาม สถานะ maintenance-mode และการไม่รองรับความสูงแบบไดนามิกทำให้ไม่เหมาะสำหรับโปรเจกต์ใหม่ที่มีความต้องการซับซ้อน

สำหรับการสร้างแอปพลิเคชัน React ที่มีประสิทธิภาพ การเข้าใจ tradeoff ของ virtualization แสดงให้เห็นความตระหนักด้านประสิทธิภาพที่ผู้สัมภาษณ์มองหา

การวัดการปรับปรุงประสิทธิภาพ

การวัดประโยชน์ของ virtualization ต้องการวิธีการวัดที่สม่ำเสมอ Performance panel ของ Chrome DevTools ให้ metrics ที่เกี่ยวข้อง

การวัดหลักก่อน/หลัง virtualization:

  • First Contentful Paint (FCP): เวลาจนกว่าเนื้อหาแรกจะ render Virtualization ลดสิ่งนี้โดยจำกัดการสร้าง DOM เริ่มต้น
  • Total Blocking Time (TBT): การ block main thread ระหว่างการโหลด DOM node น้อยลงหมายถึงการคำนวณ style/layout น้อยลง
  • การใช้หน่วยความจำ: Snapshot heap size ใน Memory panel ควรคงที่โดยไม่คำนึงถึงความยาวรายการ
  • Scroll fps: บันทึก Performance trace ขณะ scrolling Frame rate ควรอยู่ที่ 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 นี้บันทึก scroll FPS ระหว่างการพัฒนา แอปพลิเคชัน production ควรใช้เครื่องมือ observability เช่น Sentry Performance หรือ telemetry แบบกำหนดเอง

เริ่มฝึกซ้อมเลย!

ทดสอบความรู้ของคุณด้วยตัวจำลองสัมภาษณ์และแบบทดสอบเทคนิคครับ

ประเด็นสำคัญสำหรับประสิทธิภาพรายการ JavaScript

  • Virtualization ลด DOM node จากหลายพันเหลือประมาณ 20 รักษาหน่วยความจำและประสิทธิภาพ scroll คงที่โดยไม่คำนึงถึงขนาดข้อมูล
  • TanStack Virtual (12KB) ให้ primitive แบบ headless สำหรับการควบคุมสูงสุดบน React, Vue, Solid และ Svelte
  • react-virtuoso (37KB) ให้ component แบบ batteries-included พร้อมการจัดการความสูงแบบไดนามิกอัตโนมัติ
  • react-window ยังคงอยู่ใน maintenance mode ตั้งแต่ 2022 และไม่รองรับความสูงแบบไดนามิก
  • ตั้ง overscan ระหว่าง 5-10 สำหรับความลื่นของ scroll ที่เหมาะสมโดยไม่ทำให้ประโยชน์ของ virtualization หายไป
  • จองขนาดรูปภาพเพื่อป้องกันการกระโดดตำแหน่ง scroll ระหว่างการโหลดแบบ asynchronous
  • ยก state ที่ expandable/editable ขึ้นไปที่ component parent เพราะแถวที่ virtualize unmount ระหว่าง scroll
  • ใช้ key ที่คงที่จาก ID ข้อมูล ไม่ใช่ index ของ array เพื่อป้องกัน state ไม่ตรงกันเมื่อเรียงลำดับใหม่
  • วัด FCP, TBT, หน่วยความจำ และ scroll FPS ก่อน/หลังการใช้งานเพื่อวัดการปรับปรุง
ชาเลนจ์ประจำวัน

คุณหาบั๊กใน Node.js / NestJS เจอไหม

โค้ดจริงหนึ่งชิ้น บั๊กที่ซ่อนอยู่หนึ่งจุด วันละหนึ่งครั้ง ลองได้โดยไม่ต้องมีบัญชี

Anthony Fillion-Maillet

เขียนโดย

Anthony Fillion-Maillet

ผู้ก่อตั้ง SharpSkill

เป็นนักพัฒนาฟูลสแตกมากว่า 10 ปี ดูแล SharpSkill และรับผิดชอบทุกสิ่งที่เผยแพร่ที่นี่

อัปเดตเมื่อ 15 กันยายน 2569

แท็ก

#javascript
#performance
#virtualization
#react
#typescript

แชร์

บทความที่เกี่ยวข้อง

High Performance JavaScript ListView Virtualization

High Performance JavaScript ListView 2026: เทคนิค Virtualization และการเพิ่มประสิทธิภาพ

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

สถาปัตยกรรม NestJS Microservices กับ gRPC

Microservices ด้วย NestJS ปี 2026: สถาปัตยกรรม, gRPC และคำถามสัมภาษณ์งาน

คู่มือฉบับสมบูรณ์เรื่องสถาปัตยกรรม NestJS Microservices กับ gRPC: transport layer, Protocol Buffers, streaming patterns และคำถามสัมภาษณ์งานสำหรับ backend engineer ปี 2026

NestJS และ Prisma สำหรับการสร้างสแตกแบ็กเอนด์สมัยใหม่

NestJS + Prisma: สแตกแบ็กเอนด์สมัยใหม่สำหรับ Node.js

คู่มือฉบับสมบูรณ์ในการสร้าง API แบ็กเอนด์สมัยใหม่ด้วย NestJS และ Prisma ครอบคลุมการตั้งค่า โมเดล เซอร์วิส ทรานแซกชัน และแนวปฏิบัติที่ดี