Skip to content

渲染优化体系

React 性能优化的核心策略与实践

1. React 渲染机制

1.1 什么时候会重新渲染?

┌─────────────────────────────────────────────────────────────────┐
│                    触发重新渲染的情况                             │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  1. 组件自身 state 变化                                         │
│  2. 父组件重新渲染(即使 props 没变)                            │
│  3. Context 值变化                                              │
│  4. 自定义 Hook 中的 state 变化                                 │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

1.2 重新渲染 ≠ DOM 更新

jsx
// 组件重新渲染(执行函数)
// ↓
// 生成新的 Virtual DOM
// ↓
// Diff 对比
// ↓
// 只有变化的部分更新真实 DOM

重要:组件重新渲染本身开销不大,真正昂贵的是 DOM 操作。


2. React.memo

2.1 基本用法

jsx
// 只有 props 变化时才重新渲染
const ExpensiveComponent = memo(function ExpensiveComponent({ data }) {
    return <div>{/* 复杂渲染 */}</div>;
});

// 自定义比较函数
const MemoizedComponent = memo(
    function MyComponent({ user }) {
        return <div>{user.name}</div>;
    },
    (prevProps, nextProps) => {
        // 返回 true 表示 props 相等,不需要重新渲染
        return prevProps.user.id === nextProps.user.id;
    }
);

2.2 什么时候用 memo?

jsx
// ✅ 适合使用 memo 的场景
// 1. 组件渲染开销大
// 2. 父组件频繁重新渲染但 props 基本不变
// 3. 纯展示组件

// ❌ 不需要 memo 的场景
// 1. 组件渲染很快
// 2. props 经常变化
// 3. 父组件很少重新渲染

2.3 memo 的陷阱

jsx
// ❌ 错误:每次渲染都创建新对象/函数
function Parent() {
    return (
        <MemoizedChild
            style={{ color: 'red' }}     // 每次都是新对象
            onClick={() => {}}            // 每次都是新函数
        />
    );
}

// ✅ 正确:稳定的引用
function Parent() {
    const style = useMemo(() => ({ color: 'red' }), []);
    const handleClick = useCallback(() => {}, []);
    
    return <MemoizedChild style={style} onClick={handleClick} />;
}

3. useMemo 与 useCallback

3.1 useMemo - 缓存计算结果

jsx
function ProductList({ products, filter }) {
    // 只有 products 或 filter 变化时才重新计算
    const filteredProducts = useMemo(() => {
        return products.filter(p => p.category === filter);
    }, [products, filter]);
    
    return filteredProducts.map(p => <ProductCard key={p.id} {...p} />);
}

3.2 useCallback - 缓存函数

jsx
function Parent() {
    const [count, setCount] = useState(0);
    
    // 不用 useCallback:每次渲染都是新函数
    // const handleClick = () => { ... };
    
    // 用 useCallback:函数引用稳定
    const handleClick = useCallback(() => {
        console.log('clicked');
    }, []);
    
    return <MemoizedChild onClick={handleClick} />;
}

3.3 什么时候需要?

jsx
// ✅ 需要 useMemo 的场景
// 1. 计算开销大(复杂过滤、排序、转换)
// 2. 结果作为其他 Hook 的依赖
// 3. 传递给 memo 组件的对象/数组

// ✅ 需要 useCallback 的场景
// 1. 传递给 memo 组件的回调函数
// 2. 作为其他 Hook 的依赖
// 3. 传递给需要稳定引用的第三方库

// ❌ 不需要的场景
// 1. 简单计算
// 2. 不作为依赖使用
// 3. 传递给普通(非 memo)组件

4. 状态管理优化

4.1 状态下沉

jsx
// ❌ 状态放太高,导致整个组件树重新渲染
function App() {
    const [inputValue, setInputValue] = useState('');
    
    return (
        <div>
            <input value={inputValue} onChange={e => setInputValue(e.target.value)} />
            <ExpensiveList />  {/* 被迫重新渲染 */}
        </div>
    );
}

// ✅ 状态下沉到需要的地方
function App() {
    return (
        <div>
            <SearchInput />    {/* 状态在这里 */}
            <ExpensiveList />  {/* 不会重新渲染 */}
        </div>
    );
}

function SearchInput() {
    const [inputValue, setInputValue] = useState('');
    return <input value={inputValue} onChange={e => setInputValue(e.target.value)} />;
}

4.2 组件拆分

jsx
// ❌ 大组件:任何状态变化都重新渲染整个组件
function Dashboard() {
    const [sidebarOpen, setSidebarOpen] = useState(false);
    const [data, setData] = useState(null);
    
    return (
        <div>
            <Sidebar open={sidebarOpen} />
            <ExpensiveChart data={data} />  {/* sidebarOpen 变化也会重新渲染 */}
        </div>
    );
}

// ✅ 拆分组件:隔离状态影响范围
function Dashboard() {
    return (
        <div>
            <SidebarContainer />  {/* 自己管理 open 状态 */}
            <ChartContainer />    {/* 自己管理 data 状态 */}
        </div>
    );
}

4.3 Context 优化

jsx
// ❌ 任何值变化都导致所有消费者重新渲染
const AppContext = createContext({ user: null, theme: 'light', notifications: [] });

// ✅ 方案一:拆分 Context
const UserContext = createContext(null);
const ThemeContext = createContext('light');

// ✅ 方案二:useMemo 包装 value
function AppProvider({ children }) {
    const [user, setUser] = useState(null);
    const value = useMemo(() => ({ user, setUser }), [user]);
    return <UserContext.Provider value={value}>{children}</UserContext.Provider>;
}

// ✅ 方案三:使用状态管理库(Zustand、Jotai)

5. 虚拟列表

5.1 问题:大列表渲染

jsx
// ❌ 渲染 10000 个 DOM 节点
function BigList({ items }) {
    return (
        <ul>
            {items.map(item => (
                <li key={item.id}>{item.name}</li>  // 10000 个 li
            ))}
        </ul>
    );
}

5.2 虚拟列表原理

┌─────────────────────────────────────────────────────────────────┐
│                    虚拟列表原理                                   │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  ┌─────────┐                                                   │
│  │ item 1  │  ← 不渲染(在视口上方)                            │
│  │ item 2  │                                                   │
│  ├─────────┤ ─ 视口顶部 ─────────────────────                   │
│  │ item 3  │  ← 渲染                                           │
│  │ item 4  │  ← 渲染                                           │
│  │ item 5  │  ← 渲染                                           │
│  ├─────────┤ ─ 视口底部 ─────────────────────                   │
│  │ item 6  │  ← 不渲染(在视口下方)                            │
│  │ ...     │                                                   │
│  └─────────┘                                                   │
│                                                                 │
│  只渲染可视区域 + 上下缓冲区的元素                               │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

5.3 使用 react-window

jsx
import { FixedSizeList } from 'react-window';

function VirtualList({ items }) {
    const Row = ({ index, style }) => (
        <div style={style}>
            {items[index].name}
        </div>
    );
    
    return (
        <FixedSizeList
            height={400}        // 容器高度
            width="100%"        // 容器宽度
            itemCount={items.length}
            itemSize={50}       // 每项高度
        >
            {Row}
        </FixedSizeList>
    );
}

5.4 动态高度列表

jsx
import { VariableSizeList } from 'react-window';

function DynamicList({ items }) {
    const listRef = useRef();
    const sizeMap = useRef({});
    
    const setSize = useCallback((index, size) => {
        sizeMap.current[index] = size;
        listRef.current?.resetAfterIndex(index);
    }, []);
    
    const getSize = (index) => sizeMap.current[index] || 50;
    
    return (
        <VariableSizeList
            ref={listRef}
            height={400}
            itemCount={items.length}
            itemSize={getSize}
        >
            {({ index, style }) => (
                <DynamicRow 
                    data={items[index]} 
                    style={style}
                    setSize={setSize}
                    index={index}
                />
            )}
        </VariableSizeList>
    );
}

6. React DevTools Profiler

6.1 使用 Profiler

jsx
import { Profiler } from 'react';

function App() {
    const onRender = (id, phase, actualDuration) => {
        console.log({ id, phase, actualDuration });
    };
    
    return (
        <Profiler id="App" onRender={onRender}>
            <MyComponent />
        </Profiler>
    );
}

6.2 onRender 回调参数

javascript
function onRender(
    id,              // Profiler 的 id
    phase,           // "mount" 或 "update"
    actualDuration,  // 本次渲染耗时
    baseDuration,    // 无优化时预计耗时
    startTime,       // 开始渲染的时间
    commitTime       // 提交的时间
) {
    // 记录性能数据
}

6.3 React DevTools 使用

React DevTools → Profiler 面板
1. 点击录制按钮
2. 执行要分析的操作
3. 停止录制
4. 分析火焰图:
   - 灰色:未重新渲染
   - 黄色/橙色:渲染耗时
   - 越亮表示耗时越长

7. 其他优化技巧

7.1 延迟加载

jsx
// 组件懒加载
const HeavyComponent = lazy(() => import('./HeavyComponent'));

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

7.2 防抖与节流

jsx
function SearchInput() {
    const [query, setQuery] = useState('');
    
    // 防抖搜索
    const debouncedSearch = useMemo(
        () => debounce((value) => {
            fetchResults(value);
        }, 300),
        []
    );
    
    const handleChange = (e) => {
        setQuery(e.target.value);
        debouncedSearch(e.target.value);
    };
    
    return <input value={query} onChange={handleChange} />;
}

7.3 使用 key 重置组件

jsx
// 使用 key 强制重新挂载,比复杂的状态重置逻辑更清晰
<UserProfile key={userId} userId={userId} />

8. 面试高频问题

Q1: React 性能优化有哪些方法?

  1. 组件优化:memo、useMemo、useCallback
  2. 状态优化:状态下沉、Context 拆分
  3. 渲染优化:虚拟列表、懒加载
  4. 避免不必要渲染:key 使用、稳定引用

Q2: useMemo 和 useCallback 的区别?

区别useMemouseCallback
返回值缓存计算结果缓存函数本身
执行立即执行传入函数不执行,返回函数
用途计算值回调函数

Q3: 为什么不给所有组件加 memo?

  1. 比较开销:memo 需要 shallow compare props
  2. 大多数组件渲染很快:优化收益不大
  3. 增加复杂度:代码更难理解
  4. React Compiler:未来会自动优化

Q4: 如何定位性能问题?

  1. React DevTools Profiler:分析组件渲染耗时
  2. Chrome DevTools Performance:分析整体性能
  3. why-did-you-render:检测不必要的重新渲染
  4. console.log 渲染次数:简单但有效

Q5: 虚拟列表的原理?

只渲染可视区域内的元素,通过监听滚动事件动态计算应该渲染哪些元素,使用绝对定位或 transform 放置元素,实现"滚动"效果。

前端面试知识库