前端長(zhǎng)列表優(yōu)化的核心是虛擬滾動(dòng),通過只渲染可視區(qū)域內(nèi)的列表項(xiàng)提升性能。1. 固定高度虛擬滾動(dòng):適用于列表項(xiàng)高度一致的場(chǎng)景,通過計(jì)算滾動(dòng)位置確定可視區(qū)域索引并渲染;2. 動(dòng)態(tài)高度虛擬滾動(dòng):記錄每個(gè)項(xiàng)的實(shí)際高度,適應(yīng)高度不一致的情況;3. intersection observer 虛擬滾動(dòng):利用 api 精確監(jiān)聽元素是否進(jìn)入可視區(qū)域,減少無效渲染;4. react 虛擬滾動(dòng)組件:如 react-window,支持固定和動(dòng)態(tài)高度,使用便捷;5. vue 虛擬滾動(dòng)組件:如 vue-virtual-scroller,適用于 vue 項(xiàng)目。選擇方案時(shí)需考慮高度是否固定及技術(shù)棧,同時(shí)注意優(yōu)化渲染復(fù)雜度、資源加載與數(shù)據(jù)緩存以進(jìn)一步提升性能。
前端長(zhǎng)列表優(yōu)化,核心在于避免一次性渲染大量dom節(jié)點(diǎn)導(dǎo)致頁面卡頓。虛擬滾動(dòng)是關(guān)鍵,只渲染可視區(qū)域內(nèi)的列表項(xiàng),大幅提升性能。
解決方案
虛擬滾動(dòng)通過監(jiān)聽滾動(dòng)事件,動(dòng)態(tài)計(jì)算可視區(qū)域內(nèi)的列表項(xiàng),并更新DOM。本質(zhì)上,它是“按需渲染”,只渲染用戶實(shí)際看到的部分。下面提供5種虛擬滾動(dòng)方案,幫你搞定萬級(jí)列表:
-
固定高度虛擬滾動(dòng): 這是最簡(jiǎn)單的方案。假設(shè)每個(gè)列表項(xiàng)高度固定,通過滾動(dòng)條位置計(jì)算出可視區(qū)域起始和結(jié)束的索引,然后只渲染這部分?jǐn)?shù)據(jù)。
立即學(xué)習(xí)“前端免費(fèi)學(xué)習(xí)筆記(深入)”;
const list = document.getElementById('list'); const itemHeight = 50; // 每個(gè)列表項(xiàng)高度 const visibleCount = 20; // 可視區(qū)域顯示多少個(gè) const totalHeight = data.length * itemHeight; // 列表總高度 list.style.height = totalHeight + 'px'; // 設(shè)置容器高度,撐開滾動(dòng)條 function renderList(startIndex, endIndex) { // 清空現(xiàn)有列表 list.innerHTML = ''; for (let i = startIndex; i <= endIndex; i++) { const item = document.createElement('div'); item.style.height = itemHeight + 'px'; item.textContent = data[i]; list.appendChild(item); } } list.addEventListener('scroll', () => { const scrollTop = list.scrollTop; const startIndex = Math.floor(scrollTop / itemHeight); const endIndex = Math.min(startIndex + visibleCount - 1, data.length - 1); renderList(startIndex, endIndex); }); // 初始渲染 renderList(0, visibleCount - 1);
這種方案簡(jiǎn)單直接,但要求列表項(xiàng)高度一致。如果高度不一致,會(huì)導(dǎo)致計(jì)算錯(cuò)誤,體驗(yàn)很差。
-
動(dòng)態(tài)高度虛擬滾動(dòng): 解決固定高度的局限性。需要記錄每個(gè)列表項(xiàng)的實(shí)際高度,并在滾動(dòng)時(shí)動(dòng)態(tài)計(jì)算可視區(qū)域。可以使用 getBoundingClientRect() 獲取元素高度。
const list = document.getElementById('list'); const itemHeights = []; // 存儲(chǔ)每個(gè)列表項(xiàng)高度 let totalHeight = 0; function renderList(startIndex, endIndex) { list.innerHTML = ''; for (let i = startIndex; i <= endIndex; i++) { const item = document.createElement('div'); item.textContent = data[i]; list.appendChild(item); // 獲取高度并存儲(chǔ) item.onload = () => { // 確保內(nèi)容加載完畢后獲取高度 const height = item.getBoundingClientRect().height; itemHeights[i] = height; totalHeight += height; // 動(dòng)態(tài)計(jì)算總高度 list.style.height = totalHeight + 'px'; }; } } list.addEventListener('scroll', () => { const scrollTop = list.scrollTop; let startIndex = 0; let currentHeight = 0; // 計(jì)算起始索引 for (let i = 0; i < data.length; i++) { currentHeight += itemHeights[i] || 50; // 默認(rèn)高度,防止初始計(jì)算出錯(cuò) if (currentHeight >= scrollTop) { startIndex = i; break; } } let endIndex = startIndex; currentHeight = 0; // 計(jì)算結(jié)束索引 for (let i = startIndex; i < data.length; i++) { currentHeight += itemHeights[i] || 50; if (currentHeight >= scrollTop + list.clientHeight) { endIndex = i; break; } } renderList(startIndex, endIndex); }); // 初始渲染 renderList(0, Math.min(20, data.length - 1));
動(dòng)態(tài)高度的實(shí)現(xiàn)復(fù)雜一些,需要維護(hù)每個(gè)列表項(xiàng)的高度信息,但能適應(yīng)更復(fù)雜的場(chǎng)景。
-
Intersection Observer 虛擬滾動(dòng): 利用 IntersectionObserver API 監(jiān)聽元素是否進(jìn)入可視區(qū)域。 這種方式可以更精確地判斷元素是否可見,減少不必要的渲染。
const list = document.getElementById('list'); const observer = new IntersectionObserver(entries => { entries.forEach(entry => { if (entry.isIntersecting) { // 元素進(jìn)入可視區(qū)域,進(jìn)行渲染或加載數(shù)據(jù) const index = entry.target.dataset.index; entry.target.textContent = data[index]; observer.unobserve(entry.target); // 停止監(jiān)聽,避免重復(fù)渲染 } }); }); for (let i = 0; i < data.length; i++) { const item = document.createElement('div'); item.dataset.index = i; item.style.height = '50px'; // 占位高度 list.appendChild(item); observer.observe(item); // 開始監(jiān)聽 }
IntersectionObserver 的優(yōu)點(diǎn)是性能更好,可以異步監(jiān)聽,不會(huì)阻塞主線程。
-
React 虛擬滾動(dòng)組件: 如果你使用 React,有很多現(xiàn)成的虛擬滾動(dòng)組件可以使用,比如 react-window、react-virtualized。 這些組件封裝了虛擬滾動(dòng)的邏輯,使用起來非常方便。
import { FixedSizeList } from 'react-window'; const Row = ({ index, style }) => ( <div style={style}> Row {index} </div> ); const MyListComponent = () => ( <FixedSizeList height={600} itemCount={10000} itemSize={50} width={800} > {Row} </FixedSizeList> );
react-window 性能很好,支持固定高度和動(dòng)態(tài)高度的列表。
-
Vue 虛擬滾動(dòng)組件: Vue 也有類似的組件,比如 vue-virtual-scroller。 用法和 React 組件類似,可以輕松實(shí)現(xiàn)虛擬滾動(dòng)。
<template> <recycle-scroller class="list" :items="listData" :item-size="50" > <template v-slot="{ item }"> <div>{{ item }}</div> </template> </recycle-scroller> </template> <script> import { RecycleScroller } from 'vue-virtual-scroller' import 'vue-virtual-scroller/dist/vue-virtual-scroller.css' export default { components: { RecycleScroller }, data() { return { listData: Array.from({ length: 10000 }, (_, i) => `Item ${i}`) } } } </script>
選擇合適的虛擬滾動(dòng)方案,取決于你的項(xiàng)目需求和技術(shù)棧。
如何選擇合適的虛擬滾動(dòng)方案?
選擇哪種方案,主要看列表項(xiàng)的高度是否固定,以及你使用的前端框架。如果高度固定,第一種方案最簡(jiǎn)單。如果高度不固定,需要考慮動(dòng)態(tài)高度的方案或者使用現(xiàn)成的組件。
虛擬滾動(dòng)性能瓶頸在哪里?
即使使用了虛擬滾動(dòng),如果列表項(xiàng)的渲染邏輯過于復(fù)雜,仍然可能出現(xiàn)性能問題。比如,列表項(xiàng)包含大量的圖片或者復(fù)雜的計(jì)算。這時(shí)候需要優(yōu)化列表項(xiàng)的渲染邏輯,比如使用圖片懶加載、減少計(jì)算量。
除了虛擬滾動(dòng),還有其他優(yōu)化方案嗎?
除了虛擬滾動(dòng),還可以考慮以下優(yōu)化方案:
- 分頁加載: 將長(zhǎng)列表分成多個(gè)頁面,每次只加載一頁數(shù)據(jù)。
- 懶加載: 對(duì)于圖片等資源,只在需要顯示的時(shí)候才加載。
- 數(shù)據(jù)緩存: 將已經(jīng)加載的數(shù)據(jù)緩存起來,避免重復(fù)加載。
- 骨架屏: 在數(shù)據(jù)加載完成之前,顯示一個(gè)骨架屏,提升用戶體驗(yàn)。
選擇合適的優(yōu)化方案,需要根據(jù)具體的場(chǎng)景進(jìn)行分析。