React 性能优化技术

深入理解 React 性能优化技术:记忆化、懒加载、虚拟列表、防抖节流、以及如何正确测量性能。

React 性能优化技术

性能优化的原则

性能优化的第一条原则是:不要优化还没有测量过的代码

但这不意味着盲目编写低效代码。理解 React 的渲染机制和常见的性能瓶颈,可以帮助你在编写代码时做出更好的决策,并在性能问题出现时快速定位。


测量工具

React DevTools Profiler

React DevTools 的 Profiler 面板可以记录每次渲染的信息:

// 在开发环境中使用
import { profiler } from 'react';

const ProfileComponent = ({ data }) => {
  profiler.log('Rendered with data:', data);
  return <div>{data.name}</div>;
};

Performance API

const PerformanceMark = ({ name, children }) => {
  useEffect(() => {
    performance.mark(`${name}-start`);
    return () => {
      performance.mark(`${name}-end`);
      performance.measure(name, `${name}-start`, `${name}-end`);
    };
  }, [name]);

  return children;
};

User Timing API

function measureRender(componentName, renderCount) {
  performance.getEntriesByName(componentName).forEach(entry => {
    console.log(`${componentName} took ${entry.duration}ms`);
  });
}

记忆化(Memoization)

React.memo

React.memo 防止组件在 props 没变化时重新渲染:

const MemoizedComponent = React.memo(function Component({ data }) {
  return <div>{data.name}</div>;
});

// 或者带自定义比较
const MemoizedComponent = React.memo(
  Component,
  (prevProps, nextProps) => prevProps.id === nextProps.id
);

useMemo

useMemo 缓存计算结果:

function ExpensiveList({ items, filter }) {
  // 只有 items 或 filter 变化时才重新计算
  const filteredItems = useMemo(() => {
    return items
      .filter(item => item.name.includes(filter))
      .sort((a, b) => a.name.localeCompare(b.name));
  }, [items, filter]);

  return <List items={filteredItems} />;
}

useCallback

useCallback 缓存函数引用:

function Parent() {
  const [count, setCount] = useState(0);

  // 只有空依赖,函数引用稳定
  const handleClick = useCallback(() => {
    console.log('clicked');
  }, []);

  return <MemoizedChild onClick={handleClick} count={count} />;
}

懒加载(Lazy Loading)

React.lazy

React.lazy 允许你代码分割组件:

const LazyComponent = React.lazy(() => import('./HeavyComponent'));

function App() {
  return (
    <Suspense fallback={<Loading />}>
      <LazyComponent />
    </Suspense>
  );
}

命名导出

const LazyComponent = React.lazy(() =>
  import('./HeavyComponent').then(module => ({
    default: module.HeavyComponent
  }))
);

Suspense 边界

function App() {
  return (
    <div>
      <h1>Main App</h1>
      <Suspense fallback={<Spinner />}>
        <Route path="/dashboard" component={Dashboard} />
        <Route path="/settings" component={Settings} />
      </Suspense>
    </div>
  );
}

预加载

const LazyComponent = React.lazy(() => import('./HeavyComponent'));

// 在需要之前预加载
function App() {
  const handleHover = () => {
    // 用户悬停时预加载
    import('./HeavyComponent');
  };

  return (
    <div onMouseEnter={handleHover}>
      <Link to="/heavy">Go to Heavy Page</Link>
    </div>
  );
}

虚拟列表(Virtualization)

当列表项数量非常大时(如 10000+),即使渲染列表项本身不慢,DOM 节点过多也会导致性能问题。虚拟列表的思路是:只渲染可视区域内的列表项

虚拟列表原理

function VirtualList({ items, itemHeight, containerHeight }) {
  const [scrollTop, setScrollTop] = useState(0);

  const startIndex = Math.floor(scrollTop / itemHeight);
  const endIndex = Math.min(
    startIndex + Math.ceil(containerHeight / itemHeight) + 1,
    items.length
  );

  const visibleItems = items.slice(startIndex, endIndex);

  const totalHeight = items.length * itemHeight;
  const offsetY = startIndex * itemHeight;

  return (
    <div
      style={{ height: containerHeight, overflow: 'auto' }}
      onScroll={e => setScrollTop(e.target.scrollTop)}
    >
      <div style={{ height: totalHeight, position: 'relative' }}>
        <div style={{ transform: `translateY(${offsetY}px)` }}>
          {visibleItems.map((item, index) => (
            <div key={item.id} style={{ height: itemHeight }}>
              <ListItem item={item} />
            </div>
          ))}
        </div>
      </div>
    </div>
  );
}

第三方库

  • react-window:轻量级虚拟列表库
  • react-virtualized:功能更丰富的虚拟列表库
  • @tanstack/react-virtual(原 react-virtual):现代、轻量级
import { useVirtualizer } from '@tanstack/react-virtual';

function VirtualList({ items }) {
  const parentRef = useRef(null);

  const virtualizer = useVirtualizer({
    count: items.length,
    getScrollElement: () => parentRef.current,
    estimateSize: () => 50,
  });

  return (
    <div ref={parentRef} style={{ height: 400, overflow: 'auto' }}>
      <div style={{ height: virtualizer.getTotalSize() }}>
        {virtualizer.getVirtualItems().map(({ index, size, start }) => (
          <div
            key={items[index].id}
            style={{
              position: 'absolute',
              top: start,
              height: size,
            }}
          >
            <ListItem item={items[index]} />
          </div>
        ))}
      </div>
    </div>
  );
}

防抖和节流

防抖(Debounce)

防抖用于「最后一次触发」的场景:如搜索输入,用户停止输入后才执行搜索。

import { useDebouncedCallback } from 'use-debounce';

function SearchInput() {
  const [query, setQuery] = useState('');
  const [results, setResults] = useState([]);

  const debouncedSearch = useDebouncedCallback((value) => {
    fetchResults(value).then(setResults);
  }, 300);

  return (
    <div>
      <input
        value={query}
        onChange={e => {
          setQuery(e.target.value);
          debouncedSearch(e.target.value);
        }}
      />
      <SearchResults results={results} />
    </div>
  );
}

节流(Throttle)

节流用于「固定频率」的场景:如滚动事件监听、拖拽位置更新。

import { useThrottledCallback } from 'use-throttled-callback';

function ScrollTracker() {
  const [scrollY, setScrollY] = useState(0);

  const handleScroll = useThrottledCallback(() => {
    setScrollY(window.scrollY);
  }, 100);

  useEffect(() => {
    window.addEventListener('scroll', handleScroll);
    return () => window.removeEventListener('scroll', handleScroll);
  }, [handleScroll]);

  return <div>Scroll: {scrollY}</div>;
}

在 useEffect 中使用

function useDebounce(value, delay) {
  const [debouncedValue, setDebouncedValue] = useState(value);

  useEffect(() => {
    const timer = setTimeout(() => setDebouncedValue(value), delay);
    return () => clearTimeout(timer);
  }, [value, delay]);

  return debouncedValue;
}

function SearchComponent({ filter }) {
  const debouncedFilter = useDebounce(filter, 300);

  const results = useMemo(() => {
    return allItems.filter(item =>
      item.name.includes(debouncedFilter)
    );
  }, [debouncedFilter]);

  return <List items={results} />;
}

状态管理优化

状态位置

// 不好:状态放在父组件,导致不必要的重新渲染
function Parent() {
  const [count, setCount] = useState(0);

  return (
    <div>
      <ExpensiveTree />  {/* count 变化导致整个树重新渲染 */}
      <button onClick={() => setCount(c => c + 1)}>{count}</button>
    </div>
  );
}

// 好:状态放在需要它的组件
function Parent() {
  return (
    <div>
      <ExpensiveTree />
      <Counter />
    </div>
  );
}

function Counter() {
  const [count, setCount] = useState(0);
  return <button onClick={() => setCount(c => c + 1)}>{count}</button>;
}

状态拆分

// 不好:一个 state 对象包含不相关的值
const [state, setState] = useState({
  user: null,
  theme: 'light',
  notifications: [],
  isLoading: false
});

// 好:拆分成独立的状态
const [user, setUser] = useState(null);
const [theme, setTheme] = useState('light');
const [notifications, setNotifications] = useState([]);
const [isLoading, setIsLoading] = useState(false);

渲染优化

避免内联对象和函数

// 不好:每次渲染都创建新对象
function Component() {
  return <Child style={{ color: 'red' }} />;
}

// 好:对象定义在组件外或使用 useMemo
const RED_STYLE = { color: 'red' };
function Component() {
  return <Child style={RED_STYLE} />;
}

使用 CSS 而不是 JavaScript 动画

// 不好:JavaScript 动画会阻塞主线程
function ExpensiveAnimation() {
  const [pos, setPos] = useState({ x: 0, y: 0 });

  useEffect(() => {
    const handleMove = (e) => setPos({ x: e.clientX, y: e.clientY });
    window.addEventListener('mousemove', handleMove);
    return () => window.removeEventListener('mousemove', handleMove);
  }, []);

  return <div style={{ transform: `translate(${pos.x}px, ${pos.y}px)` }} />;
}

// 好:CSS transition 处理动画
function GoodAnimation() {
  return <div className="follows-mouse" />;
}
.follows-mouse {
  transition: transform 0.1s ease-out;
}

常见性能问题排查

问题一:组件莫名重新渲染

  1. 使用 React DevTools Profiler 查看是什么触发了渲染
  2. 检查 state 是否放在了不必要的父组件
  3. 检查 props 是否每次都创建新引用

问题二:列表渲染卡顿

  1. 检查是否使用了稳定的 key(不是 index)
  2. 考虑虚拟列表(如果列表很长)
  3. 检查是否有不必要的深层嵌套

问题三:输入响应慢

  1. 检查是否有 useEffect 中的 setState 导致的无限循环
  2. 考虑使用 debounce 处理频繁的输入事件
  3. 检查是否有昂贵的计算在渲染中进行

面试中的表达

面试中聊到性能优化,通常是在考察你的工程判断能力:

性能优化的核心是测量优先。不要猜测性能瓶颈,而是用 React DevTools Profiler 找到真正的问题所在。

常见的优化手段包括:记忆化(React.memo/useMemo/useCallback)避免不必要的渲染;懒加载减少初始 bundle 大小;虚拟列表处理大列表;debounce/throttle 处理高频事件。

但更重要的是理解什么时候不需要优化。简单组件的 memo 比较开销可能比渲染本身还大,这就是过早优化。遵循「先测量,再优化」的原则。


延展阅读