前端工程Technical Deep Dive
运行时性能优化 —— 长列表、虚拟滚动与 requestAnimationFrame
发布时间2026/03/29
分类前端工程
预计阅读2 分钟
作者吴长龙
*
运行时性能概述
01.内容
运行时性能概述
首屏加载优化固然重要,但运行时性能同样关键。本文聚焦于用户交互、动画、列表滚动等场景的性能优化。
长列表问题
#### 问题分析
javascript snippetjavascript
// 渲染 10000 条数据
const items = Array(10000).fill(null).map((_, i) => ({ id: i, text: `Item ${i}` }));
// 传统渲染:创建 10000 个 DOM 节点
function List() {
return (
<ul>
{items.map(item => (
<li key={item.id}>{item.text}</li>
))}
</ul>
);
}
// 问题:10000 个 DOM 节点
// - 内存占用巨大
// - 布局计算耗时
// - 滚动卡顿#### 解决方案:虚拟列表
javascript snippetjavascript
import { FixedSizeList } from 'react-window';
function VirtualList({ items }) {
const Row = ({ index, style }) => (
<div style={style}>
{items[index].text}
</div>
);
return (
<FixedSizeList
height={500}
itemCount={items.length}
itemSize={50} // 每行高度
width="100%"
>
{Row}
</FixedSizeList>
);
}#### 虚拟列表原理
code snippetcode
可视区域:500px
行高:50px
可见行数:10 行
渲染:
[0-9] 行的 DOM
+ 2 行缓冲
= 约 12 行 DOM
滚动时:
- 滚动位置变化
- 重新计算显示的行
- 复用/更新 DOM#### react-window 进阶
javascript snippetjavascript
import { VariableSizeList } from 'react-window';
// 动态高度
function VirtualListDynamic({ items }) {
const rowHeights = useRef({});
const getItemSize = (index) => rowHeights.current[index] || 50;
const setRowHeight = (index, size) => {
rowHeights.current[index] = size;
listRef.current.resetAfterIndex(index);
};
const Row = ({ index, style }) => {
const item = items[index];
return (
<div style={style}>
<ItemWithAutoHeight
onHeightChange={(h) => setRowHeight(index, h)}
>
{item.content}
</ItemWithAutoHeight>
</div>
);
};
return (
<VariableSizeList
height={500}
itemCount={items.length}
itemSize={getItemSize}
width="100%"
>
{Row}
</VariableSizeList>
);
}requestAnimationFrame 优化
#### 为什么需要 rAF?
javascript snippetjavascript
// ❌ 不好:在滚动事件中直接操作 DOM
window.addEventListener('scroll', () => {
element.style.transform = `translateY(${window.scrollY}px)`;
});
// 问题:scroll 触发频率高 (≈60fps 以上)
// 每次都触发布局计算和重绘javascript snippetjavascript
// ✅ 好:使用 requestAnimationFrame
let ticking = false;
window.addEventListener('scroll', () => {
if (!ticking) {
requestAnimationFrame(() => {
element.style.transform = `translateY(${window.scrollY}px)`;
ticking = false;
});
ticking = true;
}
});#### rAF vs setTimeout
code snippetcode
setTimeout(..., 16ms):
├── 不保证在帧开始时执行
├── 可能在帧中间执行
└── 可能丢帧
requestAnimationFrame:
├── 浏览器优化,在帧开始时执行
├── 丢帧时自动合并
└── 电池友好(后台时暂停)#### 动画优化实践
javascript snippetjavascript
// ❌ 不好:使用 left/top 进行动画
function animate() {
element.style.left = `${pos}px`; // 触发重排
pos += 1;
requestAnimationFrame(animate);
}
// ✅ 好:使用 transform
function animate() {
element.style.transform = `translateX(${pos}px`; // 只触发合成
pos += 1;
requestAnimationFrame(animate);
}#### CSS 属性与重排/重绘/合成
| 属性 | 影响 | 性能 |
|---|---|---|
| transform | 合成 | ⚡ 最快 |
| opacity | 合成 | ⚡ 最快 |
| filter | 合成 | ⚡ 快 |
| backgroundColor | 重绘 | 😐 中 |
| width/height | 重排 | 🐢 慢 |
| top/left | 重排 | 🐢 慢 |
防抖与节流
#### 防抖 (Debounce)
javascript snippetjavascript
function debounce(fn, delay) {
let timer;
return function(...args) {
clearTimeout(timer);
timer = setTimeout(() => fn.apply(this, args), delay);
};
}
// 使用:输入搜索
const handleSearch = debounce((value) => {
fetchResults(value);
}, 300);
<input onChange={(e) => handleSearch(e.target.value)} />;#### 节流 (Throttle)
javascript snippetjavascript
function throttle(fn, limit) {
let inThrottle;
return function(...args) {
if (!inThrottle) {
fn.apply(this, args);
inThrottle = true;
setTimeout(() => inThrottle = false, limit);
}
};
}
// 使用:滚动统计
const handleScroll = throttle(() => {
console.log('Scroll position:', window.scrollY);
}, 100);
window.addEventListener('scroll', handleScroll);#### 两者对比
code snippetcode
防抖:连续触发只执行最后一次
适用:输入搜索、窗口调整
节流:固定时间内只执行一次
适用:滚动统计、点击统计批量更新
#### React 批处理
javascript snippetjavascript
// React 18 自动批处理
function handleClick() {
setCount(c => c + 1); // 不触发重渲染
setFlag(f => !f); // 不触发重渲染
setData(d => ({ ...d, updated: true }));
// 函数结束后只触发一次重渲染
}#### 手动批量
javascript snippetjavascript
// 紧急更新:不批处理
function handleClick() {
import { unstable_batchedUpdates } from 'react-dom';
unstable_batchedUpdates(() => {
setCount(c => c + 1);
setFlag(f => !f);
});
}长任务优化
javascript snippetjavascript
// 问题:执行时间过长阻塞主线程
function processBigData(data) {
for (let i = 0; i < 1000000; i++) {
// 处理...
}
}
// 解决:分块执行
function processBigData(data) {
let i = 0;
function processChunk() {
const chunkSize = 1000;
const end = Math.min(i + chunkSize, data.length);
for (; i < end; i++) {
// 处理一个 chunk
}
if (i < data.length) {
requestIdleCallback(processChunk);
}
}
processChunk();
}
// 或使用 Web Worker
const worker = new Worker('worker.js');
worker.postMessage({ data });
worker.onmessage = (e) => { /* 处理结果 */ };性能优化案例
#### 1. 无限滚动列表
javascript snippetjavascript
import { useCallback, useState } from 'react';
import { useVirtualizer } from '@tanstack/react-virtual';
function InfiniteList({ loadMore }) {
const [items, setItems] = useState([]);
const parentRef = useRef(null);
const virtualizer = useVirtualizer({
count: items.length,
getScrollElement: () => parentRef.current,
estimateSize: () => 50,
overscan: 5,
});
const handleScroll = useCallback(
throttle(() => {
const { scrollTop, scrollHeight, clientHeight } = parentRef.current;
if (scrollTop + clientHeight >= scrollHeight - 100) {
loadMore(); // 加载更多
}
}, 100),
[loadMore]
);
return (
<div ref={parentRef} onScroll={handleScroll} style={{ height: '500px', overflow: 'auto' }}>
<div style={{ height: virtualizer.getTotalSize() }}>
{virtualizer.getVirtualItems().map((virtualRow) => (
<div
key={virtualRow.key}
style={{
position: 'absolute',
top: virtualRow.start,
height: virtualRow.size,
}}
>
{items[virtualRow.index].content}
</div>
))}
</div>
</div>
);
}#### 2. 拖拽优化
javascript snippetjavascript
function Draggable({ children }) {
const [position, setPosition] = useState({ x: 0, y: 0 });
const lastPos = useRef({ x: 0, y: 0 });
const isDragging = useRef(false);
const handleMouseDown = (e) => {
isDragging.current = true;
lastPos.current = { x: e.clientX, y: e.clientY };
};
const handleMouseMove = useCallback(
throttle((e) => {
if (!isDragging.current) return;
const dx = e.clientX - lastPos.current.x;
const dy = e.clientY - lastPos.current.y;
setPosition(pos => ({
x: pos.x + dx,
y: pos.y + dy,
}));
lastPos.current = { x: e.clientX, y: e.clientY };
}, 16),
[]
);
const handleMouseUp = useCallback(() => {
isDragging.current = false;
}, []);
return (
<div
onMouseDown={handleMouseDown}
onMouseMove={handleMouseMove}
onMouseUp={handleMouseUp}
onMouseLeave={handleMouseUp}
style={{
transform: `translate(${position.x}px, ${position.y}px)`,
willChange: 'transform', // 优化提示
}}
>
{children}
</div>
);
}总结
| 优化技术 | 场景 | 效果 |
|---|---|---|
| 虚拟列表 | 长列表 | DOM 从 10000 → 20 |
| transform | 动画 | 跳过重排/重绘 |
| rAF | 动画 | 同步帧率 |
| 防抖/节流 | 事件 | 减少执行次数 |
| Web Worker | 计算 | 不阻塞主线程 |
| will-change | 动画 | 提示浏览器优化 |
运行时性能优化需要结合具体场景,选择合适的方案。