Cara menggunakan TanStack Virtual untuk mempercepat daftar panjang – Beragampengetahuan
Saat Anda membuat umpan sosial, jaringan data, atau UI obrolan, semuanya terasa nyaman dengan 10 proyek tiruan. Kemudian Anda terhubung ke API nyata dan merender 50.000 baris myList.map(...)dan browser terkunci.
Masalah intinya sederhana: Anda meminta DOM melakukan terlalu banyak pekerjaan.
Virtualisasi memecahkan masalah ini dengan hanya menampilkan apa yang sebenarnya dilihat pengguna. Daripada memasang 50.000 node, Anda membuat 15-20 item terlihat di area pandang dan buffer kecil. Browser kini hanya dapat mengelola beberapa lusin elemen dalam satu waktu.
TanStack Virtual melakukan pekerjaan berat untuk model ini. Ini adalah utilitas virtualisasi tanpa kepala modern: menangani matematika bergulir, penghitungan ukuran, dan pemosisian item, sementara Anda memiliki kendali penuh atas markup dan gaya.
Dalam artikel ini, Anda akan membuat feed live chat yang realistis dan berkinerja tinggi dari awal. Umpan ini akan:
- Render ribuan pesan
- Mendukung tinggi baris dinamis (pesan panjang dan pesan pendek)
- Muat riwayat lama saat menggulir ke atas (membalikkan gulir tak terbatas)
Contents
🚀 Berlangganan Buletin Putar Ulang
memutar ulang adalah buletin mingguan untuk para pemimpin pengembangan dan teknik.
Diterbitkan setiap minggu, ini adalah panduan pilihan Anda untuk percakapan paling penting tentang pengembangan front-end, alat AI yang sedang berkembang, dan keadaan perangkat lunak modern.
Pengaturan proyek
Mulai proyek React + TypeScript baru menggunakan Vite:
npx create-vite@latest tanstack-virtual-chat --template react-ts cd tanstack-virtual-chat
Anda hanya memerlukan dua dependensi tambahan:
@tanstack/react-virtualuntuk virtualisasi@faker-js/fakerUntuk menghasilkan kumpulan data yang besar dan realistis
Instal mereka:
npm install @tanstack/react-virtual @faker-js/faker
Kemudian mulai server pengembangan:
npm run dev
Anda akan melihat halaman awal Vite + React default.
Tentukan wadah gulir
Virtualisasi tidak akan berfungsi kecuali virtualizer mengetahui area pandang mana yang menjadi tanggung jawabnya. Ini berarti Anda harus menyediakan:
- wadah bergulir dengan ketinggian tetap
- gaya overflow (biasanya
overflow-y: auto)
Ganti konten src/App.css Dan:
/* src/App.css */ body font-family: sans-serif; padding: 2rem; display: grid; place-items: center; min-height: 100vh; .chat-container /* You MUST provide a defined height. */ height: 600px; width: 400px; /* You MUST provide an overflow property. */ overflow-y: auto; border: 1px solid #ccc; border-radius: 8px; .chat-bubble padding: 12px 16px; border-bottom: 1px solid #eee; display: flex; gap: 8px; .chat-bubble strong color: #333; min-width: 70px; .chat-bubble p margin: 0; color: #555; /* Allow long messages to wrap */ word-break: break-word;
Anda dapat menghapus gaya default src/index.css.
Selanjutnya, siapkan shell viewport sederhana src/App.tsx:
// src/App.tsx
import './App.css'
function App()
return (
<div>
<h1>Livestream Chat Feed</h1>
/* This div is our scrollable viewport */
<div className="chat-container">
/* We will render our virtualized list here */
</div>
</div>
)
export default App
Pada titik ini, browser akan menampilkan judul dan kotak kosong 600 piksel. Ini adalah area pandang yang akan digunakan mesin virtual.
Hasilkan kumpulan data yang besar
Untuk menunjukkan masalah kinerja, Anda memerlukan pesan dalam jumlah besar. Gunakan Faker untuk menghasilkan 10.000 pesan obrolan.
membuat src/utils.ts:
// src/utils.ts
import faker from '@faker-js/faker'
export type ChatMessage =
id: string
author: string
message: string
const createRandomMessage = (): ChatMessage =>
return
id: faker.string.uuid(),
author: faker.person.firstName(),
message: faker.lorem.sentences( min: 1, max: 15 ),
// Create a massive list of 10,000 messages
export const allMessages = Array.from( length: 10_000 , createRandomMessage)
Anda sekarang memiliki:
- antarmuka pengguna cangkang
- Gaya dasar
- satu
allMessagesArray berisi 10.000 item
Selanjutnya, Anda akan mempelajari mengapa Anda memerlukan virtualisasi.
Menerapkan mesin virtual
Langkah 1: Render semuanya (dan hancurkan)
Pertama, render seluruh daftar App.tsx Gunakan secara teratur .map():
// src/App.tsx
import './App.css'
import allMessages from './utils'
function App()
return (
<div>
<h1>Livestream Chat Feed</h1>
<div className="chat-container">
allMessages.map((msg) => (
<div key=msg.id className="chat-bubble">
<strong>msg.author</strong>
<p>msg.message</p>
</div>
))
</div>
</div>
)
export default App
Simpan dan muat ulang. Browser mungkin terhenti, mogok, atau setidaknya memerlukan waktu lama untuk merespons. Anda mencoba membuat 10.000 simpul DOM sekaligus.
Ini adalah garis dasar yang Anda optimalkan.
Langkah 2: Pengkabelan useVirtualizer
Sekarang integrasikan TanStack Virtual dan sambungkan ke rolling container Anda.
// src/App.tsx
import './App.css'
import React from 'react'
import allMessages from './utils'
import useVirtualizer from '@tanstack/react-virtual'
function App() null>(null)
const rowVirtualizer = useVirtualizer(
count: allMessages.length, // Total number of items
getScrollElement: () => parentRef.current, // The scrolling element
estimateSize: () => 50, // Approximate row height
)
return (
<div>
<h1>Livestream Chat Feed</h1>
<div ref=parentRef className="chat-container">
/* We will render the virtual items here */
</div>
</div>
)
export default App
Anda sekarang telah memberi tahu mesin virtual:
- Berapa banyak proyek yang ada
- Elemen mana yang dapat di-scroll
- Perkiraan tinggi awal untuk setiap baris
Langkah 3: Render item virtual saja
TanStack Virtual tidak memiliki kepala. Ini memberi Anda:
- satu
getTotalSize()Fungsi yang mengembalikan tinggi daftar penuh (untuk scrollbar) - satu
getVirtualItems()Array yang mendeskripsikan baris yang terlihat saat ini
Untuk menggunakannya Anda memerlukan dua aturan CSS:
- Sebuah “Pengukur”
<div>Danposition: relativeDanheightdiatur kegetTotalSize() - Anak-anak yang diposisikan secara mutlak menggunakan terjemahan vertikal
virtualItem.start
Ini adalah versi lengkap App.tsx Hubungkan virtualisasi dasar:
// src/App.tsx
import './App.css'
import React from 'react'
import allMessages from './utils'
import useVirtualizer from '@tanstack/react-virtual'
function App() {
const parentRef = React.useRef<HTMLDivElement | null>(null)
const rowVirtualizer = useVirtualizer(
count: allMessages.length,
getScrollElement: () => parentRef.current,
// A more realistic estimate for our chat bubbles
estimateSize: () => 88,
)
const virtualItems = rowVirtualizer.getVirtualItems()
return (
<div>
<h1>Livestream Chat Feed</h1>
<div ref=parentRef className="chat-container">
/* Sizer div */
<div
style=
height: `$rowVirtualizer.getTotalSize()px`,
width: '100%',
position: 'relative',
>
/* Only render the virtual items */
virtualItems.map((virtualItem) =>
const message = allMessages[virtualItem.index]
return (
<div
key=message.id
style=
position: 'absolute',
top: 0,
left: 0,
width: '100%',
transform: `translateY($virtualItem.startpx)`,
>
<div className="chat-bubble">
<strong>message.author</strong>
<p>message.message</p>
</div>
</div>
)
)
</div>
</div>
</div>
)
}
export default App
Muat ulang halaman. Menggulir 10.000 item kini akan terasa instan dan lancar karena DOM hanya berisi item yang terlihat.
Ada juga masalah: semua baris dianggap memiliki perkiraan ketinggian yang sama, sehingga pesan yang panjang terpotong.
Menangani ketinggian baris dinamis
Sekarang Anda memberi tahu mesin virtual bahwa setiap baris berada 88px Tinggi:
estimateSize: () => 88
Dalam umpan obrolan nyata, panjang dan tinggi pesan sangat bervariasi. Untuk mengatasi masalah ini, TanStack Virtual dapat mengukur setiap node setelah rendering.
Anda memerlukan dua hal:
- satu
measureElementfungsi diteruskan keuseVirtualizer - satu
refDandata-indexpada setiap elemen yang diukur
memperbarui App.tsx:
// src/App.tsx
import './App.css'
import React from 'react'
import allMessages from './utils'
import useVirtualizer from '@tanstack/react-virtual'
function App() {
const parentRef = React.useRef<HTMLDivElement | null>(null)
const rowVirtualizer = useVirtualizer(
count: allMessages.length,
getScrollElement: () => parentRef.current,
estimateSize: () => 88,
measureElement: (element) => element.getBoundingClientRect().height,
)
const virtualItems = rowVirtualizer.getVirtualItems()
return (
<div>
<h1>Livestream Chat Feed</h1>
<div ref=parentRef className="chat-container">
<div
style=
height: `$rowVirtualizer.getTotalSize()px`,
width: '100%',
position: 'relative',
>
virtualItems.map((virtualItem) =>
const message = allMessages[virtualItem.index]
return (
<div
key=message.id
data-index=virtualItem.index
ref=rowVirtualizer.measureElement
style=
position: 'absolute',
top: 0,
left: 0,
width: '100%',
transform: `translateY($virtualItem.startpx)`,
>
<div className="chat-bubble">
<strong>message.author</strong>
<p>message.message</p>
</div>
</div>
)
)
</div>
</div>
</div>
)
}
export default App
Setiap baris sekarang diukur setelah rendering, dan virtualizer memperbarui peta dimensi internalnya. Daftar tetap lancar, sementara pesan panjang dan pendek ditempatkan dengan benar.

Tambahkan pemuatan tak terbatas gaya obrolan
Langkah terakhir adalah beralih dari daftar statis ke pengalaman obrolan nyata:
- Anda awalnya hanya memuat berita terbaru
- Saat pengguna menggulir ke atas, Anda mendapatkan riwayat yang lebih lama
- Pada pemuatan awal, tampilan mulai bergulir ke bawah
Ini melibatkan tiga perubahan utama:
- merawat
allMessagesBertindak sebagai database dan menjaga keadaan jendela pesan - Tirukan API “dapatkan lebih banyak”, dengan menambahkan pesan lama ke depan
- Baris pemuatan terintegrasi dan gulir awal ke efek bawah
Langkah 1: Status pesan dan metadata paging
Refaktorisasi App Dengan cara ini Anda hanya menyimpan status 100 pesan terbaru, dan beberapa metadata tentang pemuatan:
// src/App.tsx
import './App.css'
import React from 'react'
import allMessages, type ChatMessage from './utils'
import useVirtualizer from '@tanstack/react-virtual'
// Pretend this is our "database"
const dbMessages = allMessages
const LATEST_MESSAGES_COUNT = 100
function App()
const [messages, setMessages] = React.useState<ChatMessage[]>(
dbMessages.slice(dbMessages.length - LATEST_MESSAGES_COUNT)
)
const [isFetching, setIsFetching] = React.useState(false)
const [hasNextPage, setHasNextPage] = React.useState(
messages.length < dbMessages.length
)
// More logic will go here
Langkah 2: Mock API untuk mendapatkan riwayat yang lebih lama
Selanjutnya, tambahkan fungsi yang berpura-pura menerima pesan lama. Ia menemukan pesan terlama saat ini dbMessageslalu ambil 100 yang pertama dan letakkan di depan untuk dideklarasikan.
function App() {
const [messages, setMessages] = React.useState<ChatMessage[]>(
dbMessages.slice(dbMessages.length - LATEST_MESSAGES_COUNT)
)
const [isFetching, setIsFetching] = React.useState(false)
const [hasNextPage, setHasNextPage] = React.useState(
messages.length < dbMessages.length
)
const fetchMoreMessages = React.useCallback(() =>
if (isFetching) return
setIsFetching(true)
setTimeout(() =>
const currentOldestMessage = messages[0]
const oldestMessageIndex = dbMessages.findIndex(
(msg) => msg.id === currentOldestMessage.id
)
const newOldestIndex = Math.max(0, oldestMessageIndex - 100)
const newMessages = dbMessages.slice(newOldestIndex, oldestMessageIndex)
setMessages((prev) => [...newMessages, ...prev])
if (newOldestIndex === 0)
setHasNextPage(false)
setIsFetching(false)
, 1000)
, [isFetching, messages])
ini setTimeout Simulasikan latensi jaringan.
Langkah 3: Integrasikan mesin virtual dengan garis beban
Sekarang sambungkan virtualizer ke model paging ini.
daftar count menjadi messages.length + 1 Jika ada halaman riwayat lain (baris tambahannya adalah pemuat). Jika tidak, itu hanya saja messages.length:
const parentRef = React.useRef<HTMLDivElement | null>(null)
const hasScrolledRef = React.useRef(false)
const rowVirtualizer = useVirtualizer(
count: hasNextPage ? messages.length + 1 : messages.length,
getScrollElement: () => parentRef.current,
estimateSize: () => 88,
measureElement: (element) => element.getBoundingClientRect().height,
)
const virtualItems = rowVirtualizer.getVirtualItems()
Langkah 4: Picu pengambilan saat pengguna menggulir ke atas
Menambahkan efek menonton baris virtual pertama yang terlihat. jika itu index Ya 0Anda telah mencapai daftar teratas. Jika masih ada halaman lagi dan Anda belum mendapatkannya, silakan hubungi fetchMoreMessages:
React.useEffect(() =>
const [firstItem] = virtualItems
if (!firstItem) return
if (firstItem.index === 0 && hasNextPage && !isFetching)
fetchMoreMessages()
, [virtualItems, hasNextPage, isFetching, fetchMoreMessages])
Langkah 5: Gulir ke bawah pada pemuatan awal
UI obrolan defaultnya adalah berita terbaru. Gunakan efek kedua untuk menggulir ke pesan nyata terakhir satu kali saat diinstal:
React.useEffect(() =>
if (virtualItems.length > 0 && !hasScrolledRef.current)
const lastMessageIndex = hasNextPage ? messages.length : messages.length - 1
rowVirtualizer.scrollToIndex(lastMessageIndex, align: 'end' )
hasScrolledRef.current = true
, [virtualItems, rowVirtualizer, messages.length, hasNextPage])
Langkah 6: Render pesan dan baris pemuat
Kapan hasNextPage benar, baris maya terletak di index === 0 Dicadangkan untuk pemuat (“Memuat pesan lama…”). Pesan sebenarnya dimulai dari indeks 1.
Lebih banyak artikel bagus dari beragampengetahuan:
Anda dapat menghitung indeks pesan yang benar seperti ini:
const messageIndex = hasNextPage ? virtualItem.index - 1 : virtualItem.index
Ini adalah finalnya App Komponen dengan semua bagian terhubung bersama:
// src/App.tsx
import './App.css'
import React from 'react'
import allMessages, type ChatMessage from './utils'
import useVirtualizer from '@tanstack/react-virtual'
const dbMessages = allMessages
const LATEST_MESSAGES_COUNT = 100
function App() {
const [messages, setMessages] = React.useState<ChatMessage[]>(
dbMessages.slice(dbMessages.length - LATEST_MESSAGES_COUNT)
)
const [isFetching, setIsFetching] = React.useState(false)
const [hasNextPage, setHasNextPage] = React.useState(
messages.length < dbMessages.length
)
const parentRef = React.useRef<HTMLDivElement | null>(null)
const hasScrolledRef = React.useRef(false)
const fetchMoreMessages = React.useCallback(() =>
if (isFetching) return
setIsFetching(true)
setTimeout(() =>
const currentOldestMessage = messages[0]
const oldestMessageIndex = dbMessages.findIndex(
(msg) => msg.id === currentOldestMessage.id
)
const newOldestIndex = Math.max(0, oldestMessageIndex - 100)
const newMessages = dbMessages.slice(newOldestIndex, oldestMessageIndex)
setMessages((prev) => [...newMessages, ...prev])
if (newOldestIndex === 0)
setHasNextPage(false)
setIsFetching(false)
, 1000)
, [isFetching, messages])
const rowVirtualizer = useVirtualizer(
count: hasNextPage ? messages.length + 1 : messages.length,
getScrollElement: () => parentRef.current,
estimateSize: () => 88,
measureElement: (element) => element.getBoundingClientRect().height,
)
const virtualItems = rowVirtualizer.getVirtualItems()
React.useEffect(() =>
const [firstItem] = virtualItems
if (!firstItem) return
if (firstItem.index === 0 && hasNextPage && !isFetching)
fetchMoreMessages()
, [virtualItems, hasNextPage, isFetching, fetchMoreMessages])
React.useEffect(() =>
if (virtualItems.length > 0 && !hasScrolledRef.current)
const lastMessageIndex = hasNextPage ? messages.length : messages.length - 1
rowVirtualizer.scrollToIndex(lastMessageIndex, align: 'end' )
hasScrolledRef.current = true
, [virtualItems, rowVirtualizer, messages.length, hasNextPage])
return (
<div>
<h1>Livestream Chat Feed</h1>
<div ref=parentRef className="chat-container">
<div
style=
height: `$rowVirtualizer.getTotalSize()px`,
width: '100%',
position: 'relative',
>
{virtualItems.map((virtualItem) =>
const isLoaderRow = virtualItem.index === 0 && hasNextPage
const messageIndex = hasNextPage
? virtualItem.index - 1
: virtualItem.index
const message = messages[messageIndex]
return (
<div
key=isLoaderRow ? 'loader' : message?.id ?? virtualItem.index
data-index=virtualItem.index
ref=rowVirtualizer.measureElement
style=
position: 'absolute',
top: 0,
left: 0,
width: '100%',
transform: `translateY($virtualItem.startpx)`,
>
isLoaderRow ? (
<div className="chat-bubble" style= textAlign: 'center' >
<strong>Loading older messages...</strong>
</div>
) : message ? (
<div className="chat-bubble">
<strong>message.author:</strong>
<p>message.message</p>
</div>
) : null
</div>
)
)}
</div>
</div>
</div>
)
}
export default App
Setelah melakukan ini, Anda akan mendapatkan:
- Performa tinggi, daftar virtualisasi
- Tinggi baris dinamis yang akurat
- Membalikkan gulir tak terbatas, memuat riwayat saat menggulir ke atas
- Anchor ke posisi scroll awal berita terkini

sebagai kesimpulan
Anda memulai dengan daftar besar yang akan menyebabkan browser terhenti, dan berakhir dengan ribuan baris dinamis yang disajikan dalam umpan obrolan yang bergulir mulus tanpa batas.
Pola intinya adalah:
- Sediakan wadah gulir dengan ketinggian tetap untuk mesin virtual
overflow: auto - menggunakan
useVirtualizerRender hanya baris yang terlihat ke dalam wadah bagian dalam dengan ukuran yang ditentukan - menggunakan
measureElementSaat baris Anda memiliki tinggi dinamis - Melapisi penomoran halaman dan memuat baris untuk pengguliran tanpa batas
Pendekatan ini dapat digeneralisasikan di luar obrolan. Daftar panjang, feed, atau grid apa pun di front-end dapat menggunakan mode TanStack Virtual yang sama agar tetap responsif, bahkan dalam skala besar.
rencana pengembangan website
metode pengembangan website
jelaskan beberapa rencana untuk pengembangan website, proses pengembangan website, kekuatan dan kelemahan bisnis pengembangan website
, jasa pengembangan website, tahap pengembangan website, biaya pengembangan website
#Cara #menggunakan #TanStack #Virtual #untuk #mempercepat #daftar #panjang