# Fast JavaScript ListView 2026: เทคนิค Windowing, Virtualization และการเพิ่มประสิทธิภาพ > เชี่ยวชาญเทคนิค virtualization รายการ JavaScript ด้วย TanStack Virtual, react-virtuoso และ react-window เรียนรู้ windowing เพื่อ render 100,000+ รายการที่ 60fps พร้อมตัวอย่างโค้ด - Published: 2026-09-15 - Updated: 2026-09-15 - Author: Anthony Fillion-Maillet - Tags: javascript, performance, virtualization, react, typescript - Reading time: 9 min --- การ 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 ```typescript // VirtualizedList.tsx 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(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 (
{virtualizer.getVirtualItems().map((virtualRow) => { const item = items[virtualRow.index] return (

{item.name}

{item.email}

) })}
) } ``` การใช้งานนี้ render เฉพาะแถวที่มองเห็นบวกกับ buffer overscan container ด้านนอกกำหนดความสูง scrollable ทั้งหมด ทำให้ scrollbar ทำงานเหมือนว่ารายการทั้งหมดมีอยู่ใน DOM ## TanStack Virtual: แนวทาง Headless สำหรับปี 2026 [TanStack Virtual](https://tanstack.com/virtual/latest) ให้ 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 นอก viewport | 1 | | `getScrollElement` | คืนค่า container ที่ scrollable | required | | `measureElement` | ฟังก์ชันการวัดแบบกำหนดเองสำหรับขนาดแบบไดนามิก | auto | ค่า `overscan` ส่งผลโดยตรงต่อความลื่นของการ scroll การตั้งค่าต่ำเกินไป (0-2) ทำให้เกิด flash สีขาวระหว่างการ scroll เร็ว การตั้งค่าสูงเกินไป (20+) ทำให้ประโยชน์ของ virtualization หายไป สำหรับแอปพลิเคชันส่วนใหญ่ 5-10 ให้ความสมดุลที่เหมาะสม ```typescript // DynamicHeightList.tsx 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(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 (
{virtualizer.getVirtualItems().map((virtualRow) => { const message = messages[virtualRow.index] return (

{new Date(message.timestamp).toLocaleTimeString()}

{message.content}

) })}
) } ``` Ref `measureElement` ทำให้สามารถวัดความสูงอัตโนมัติหลังจาก render ซึ่งจัดการกับความยาวเนื้อหาที่แตกต่างกันโดยไม่ต้องคำนวณความสูงด้วยตนเอง ## react-virtuoso: ทางเลือกที่มีฟีเจอร์ครบครัน [react-virtuoso](https://virtuoso.dev/) ใช้แนวทางตรงข้าม: ไลบรารี 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 ```typescript // VirtuosoList.tsx import { Virtuoso } from 'react-virtuoso' interface User { id: string name: string department: string } export function UserDirectory({ users }: { users: User[] }) { return ( (
{user.name.charAt(0)}

{user.name}

{user.department}

)} /> ) } ``` นี่ให้ผลลัพธ์ virtualization เดียวกันด้วยโค้ดที่น้อยกว่า react-virtuoso วัดรายการอัตโนมัติ จัดการความสูงแบบไดนามิก และให้ persistence ตำแหน่ง scroll สำหรับ[การเตรียมตัวสัมภาษณ์เกี่ยวกับประสิทธิภาพ React](/technologies/react-next/interview-questions/styling-css-in-js) การเข้าใจว่าเมื่อไหร่ควรเลือกแต่ละไลบรารีแสดงให้เห็นความตระหนักด้านสถาปัตยกรรม TanStack Virtual เหมาะกับแอปพลิเคชันที่ใช้ TanStack Table อยู่แล้วหรือต้องการโค้ดที่ไม่ขึ้นกับ framework react-virtuoso เหมาะกับทีมที่ให้ความสำคัญกับความเร็วในการพัฒนามากกว่าขนาด bundle ## ปัญหาที่พบบ่อยและวิธีแก้ไข ### ตำแหน่ง Scroll กระโดดเมื่อมีรูปภาพ รูปภาพโหลดแบบ asynchronous เปลี่ยนความสูงแถวหลังจากการวัดครั้งแรก Virtualizer ไม่สามารถทำนายการเปลี่ยนแปลงนี้ได้ ทำให้เนื้อหาด้านล่างเลื่อน วิธีแก้ไข: จองพื้นที่สำหรับรูปภาพโดยใช้ aspect-ratio หรือขนาดคงที่ ```typescript // ImageListItem.tsx interface ImageItem { id: string src: string title: string } export function ImageListItem({ item }: { item: ImageItem }) { return (
{/* Reserve 80x80px space regardless of load state */}
{item.title}

{item.title}

) } ``` ### State หายไประหว่าง Scroll รายการที่ virtualize unmount เมื่อ scroll ออกจากมุมมอง state local ใดๆ (input ฟอร์ม, accordion ที่ขยาย) หายไป วิธีแก้ไข: ยก state ขึ้นไปที่ component parent หรือไลบรารี state management โดย key ตาม ID ของรายการ ```typescript // ExpandableList.tsx 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>(new Set()) const parentRef = useRef(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 (
{virtualizer.getVirtualItems().map((virtualRow) => { const faq = faqs[virtualRow.index] const isExpanded = expandedIds.has(faq.id) return (
) })}
) } ``` ### 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
...
// id from database/API })} // Wrong: index changes when list reorders {virtualizer.getVirtualItems().map((virtualRow, index) => { return
...
// causes bugs on reorder })} ``` ## การเปรียบเทียบไลบรารีสำหรับโปรเจกต์ 2026 การเลือกไลบรารี virtualization ที่เหมาะสมขึ้นอยู่กับความต้องการของโปรเจกต์ การเปรียบเทียบนี้สะท้อน ecosystem ปี 2026: | เกณฑ์ | TanStack Virtual | react-virtuoso | react-window | |-------|-----------------|----------------|---------------| | ขนาด bundle | 12KB | 37KB | 6KB | | ความสูงแบบไดนามิก | ตั้งค่าด้วยตนเอง | อัตโนมัติ | ไม่รองรับ | | TypeScript | Native | Native | package @types | | รองรับ framework | React, Vue, Solid, Svelte | React เท่านั้น | React เท่านั้น | | การบำรุงรักษา | Active (2026) | Active (2026) | Maintenance mode | | ความยากในการเรียนรู้ | สูงกว่า | ต่ำกว่า | ต่ำสุด | react-window ยังคงใช้ได้สำหรับรายการ fixed-height อย่างง่ายที่ขนาด bundle สำคัญ อย่างไรก็ตาม สถานะ maintenance-mode และการไม่รองรับความสูงแบบไดนามิกทำให้ไม่เหมาะสำหรับโปรเจกต์ใหม่ที่มีความต้องการซับซ้อน สำหรับ[การสร้างแอปพลิเคชัน React ที่มีประสิทธิภาพ](/blog/react-next/react-testing-2026-vitest-rtl-best-practices) การเข้าใจ 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 ```typescript // PerformanceMonitor.tsx import { useEffect, useRef } from 'react' export function useScrollFPS(containerRef: React.RefObject) { 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 ก่อน/หลังการใช้งานเพื่อวัดการปรับปรุง --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/th/blog/node-nestjs/fast-javascript-listview-2026-windowing-virtualization-performance