前端工程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动画提示浏览器优化

运行时性能优化需要结合具体场景,选择合适的方案。