React有哪些性能优化的方法

React 性能优化指南

在 React 开发中,优化性能是提升用户体验的关键。本文将从三个主要方向探讨 React 性能优化的方法,这些方法在其他软件开发领域同样适用。

三大优化方向

  1. 减少计算量:对应到 React 中,就是减少渲染的节点或降低组件渲染的复杂度。
  2. 利用缓存:对应到 React 中,就是如何避免重新渲染,利用函数式编程的 memo 方式来避免组件重新渲染。
  3. 精确重新计算的范围:对应到 React 中,就是绑定组件和状态关系,精确判断更新的“时机”和“范围”,只重新渲染“脏”的组件,或者说降低渲染范围。

减少渲染的节点/降低渲染计算量(复杂度)

首先从减少计算量入手,减少节点渲染的数量或降低渲染的计算量可以显著提高组件渲染性能。

1. 不要在渲染函数中进行不必要的计算

避免在渲染函数(render)中进行数组排序、数据转换、订阅事件、创建事件处理器等操作。渲染函数中不应放置过多副作用。

// Bad
const MyComponent = ({ items }) => {
  return (
    <div>
      {items.sort((a, b) => a.value - b.value).map(item => (
        <div key={item.id}>{item.value}</div>
      ))}
    </div>
  );
};

// Good
const MyComponent = ({ items }) => {
  const sortedItems = useMemo(() => {
    return items.sort((a, b) => a.value - b.value);
  }, [items]);

  return (
    <div>
      {sortedItems.map(item => (
        <div key={item.id}>{item.value}</div>
      ))}
    </div>
  );
};
// Bad
const MyComponent = ({ items }) => {
  return (
    <div>
      {items.sort((a, b) => a.value - b.value).map(item => (
        <div key={item.id}>{item.value}</div>
      ))}
    </div>
  );
};

// Good
const MyComponent = ({ items }) => {
  const sortedItems = useMemo(() => {
    return items.sort((a, b) => a.value - b.value);
  }, [items]);

  return (
    <div>
      {sortedItems.map(item => (
        <div key={item.id}>{item.value}</div>
      ))}
    </div>
  );
};

2. 减少不必要的嵌套

避免过度使用 styled-components,特别是在纯静态样式规则和需要重度性能优化的场景中。过度嵌套不仅影响性能,还会带来节点嵌套地狱的问题。

// Bad
const Container = styled.div`
  div {
    span {
      color: red;
    }
  }
`;

// Good
const Container = styled.div`
  .text {
    color: red;
  }
`;

// Usage
<Container>
  <div>
    <span className="text">Hello World</span>
  </div>
</Container>
// Bad
const Container = styled.div`
  div {
    span {
      color: red;
    }
  }
`;

// Good
const Container = styled.div`
  .text {
    color: red;
  }
`;

// Usage
<Container>
  <div>
    <span className="text">Hello World</span>
  </div>
</Container>

利用缓存避免重新渲染

缓存是提高性能的有效手段,React 提供了多种方法来实现缓存,从而避免不必要的重新渲染。

1. 使用 React.memo

React.memo 可以防止无状态组件在 props 没有变化时重新渲染。

const MyComponent = React.memo(({ value }) => {
  return <div>{value}</div>;
});
const MyComponent = React.memo(({ value }) => {
  return <div>{value}</div>;
});

2. 使用 Hooks 进行性能优化

  • useCallback:返回一个 memoized 回调函数,避免函数在每次渲染时都重新创建。
  • useMemo:返回一个 memoized 值,避免在每次渲染时都重新计算值。
const ParentComponent = () => {
  const [count, setCount] = useState(0);

  const handleClick = useCallback(() => {
    setCount(prevCount => prevCount + 1);
  }, []);

  return (
    <div>
      <button onClick={handleClick}>Increment</button>
      <ChildComponent count={count} />
    </div>
  );
};

const ChildComponent = React.memo(({ count }) => {
  console.log('ChildComponent render');
  return <div>{count}</div>;
});
const ParentComponent = () => {
  const [count, setCount] = useState(0);

  const handleClick = useCallback(() => {
    setCount(prevCount => prevCount + 1);
  }, []);

  return (
    <div>
      <button onClick={handleClick}>Increment</button>
      <ChildComponent count={count} />
    </div>
  );
};

const ChildComponent = React.memo(({ count }) => {
  console.log('ChildComponent render');
  return <div>{count}</div>;
});

精确重新计算的范围

通过精确控制组件的重新渲染范围,可以大幅提升性能。

1. 使用合适的键(keys)

在列表渲染中,确保使用稳定且唯一的键,以帮助 React 高效地识别和更新组件。

// Bad
{items.map((item, index) => (
  <ListItem key={index} item={item} />
))}

// Good
{items.map((item) => (
  <ListItem key={item.id} item={item} />
))}
// Bad
{items.map((item, index) => (
  <ListItem key={index} item={item} />
))}

// Good
{items.map((item) => (
  <ListItem key={item.id} item={item} />
))}

2. 使用 shouldComponentUpdateReact.PureComponent

在类组件中,通过 shouldComponentUpdate 方法控制组件是否需要重新渲染,或使用 React.PureComponent,它会自动实现 shouldComponentUpdate,仅在 props 或 state 发生变化时重新渲染。

class MyComponent extends React.PureComponent {
  render() {
    return <div>{this.props.value}</div>;
  }
}
class MyComponent extends React.PureComponent {
  render() {
    return <div>{this.props.value}</div>;
  }
}

其他优化技巧

  1. 虚拟化长列表:使用如 react-windowreact-virtualized 这样的库,仅渲染可见的部分,避免一次性渲染大量 DOM 节点。
import { FixedSizeList as List } from 'react-window';

const Row = ({ index, style }) => (
  <div style={style}>Row {index}</div>
);

const MyList = () => (
  <List
    height={150}
    itemCount={1000}
    itemSize={35}
    width={300}
  >
    {Row}
  </List>
);
import { FixedSizeList as List } from 'react-window';

const Row = ({ index, style }) => (
  <div style={style}>Row {index}</div>
);

const MyList = () => (
  <List
    height={150}
    itemCount={1000}
    itemSize={35}
    width={300}
  >
    {Row}
  </List>
);
  1. 拆分代码(Code Splitting):使用动态 import 和 React.lazy,结合 React.Suspense,按需加载组件,减少初始加载时间。
import React, { lazy, Suspense } from 'react';

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

const App = () => (
  <Suspense fallback={<div>Loading...</div>}>
    <LazyComponent />
  </Suspense>
);
import React, { lazy, Suspense } from 'react';

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

const App = () => (
  <Suspense fallback={<div>Loading...</div>}>
    <LazyComponent />
  </Suspense>
);
  1. 避免匿名函数和对象的重新创建:在 JSX 中避免直接使用匿名函数或对象,因为每次渲染都会创建新的实例,导致子组件重新渲染。
// Bad
<Child onClick={() => doSomething()} />

// Good
const handleClick = useCallback(() => doSomething(), []);
<Child onClick={handleClick} />
// Bad
<Child onClick={() => doSomething()} />

// Good
const handleClick = useCallback(() => doSomething(), []);
<Child onClick={handleClick} />
  1. 减少 Refs 的使用:避免不必要地使用 refs,除非有明确的需要。尽量依赖于 state 和 props 管理数据。

  2. 适当地使用 Context:尽量避免在频繁变化的数据中使用 Context,因为它会导致所有消费该上下文的组件重新渲染。

// Bad
const ThemeContext = React.createContext();

const ParentComponent = () => {
  const [theme, setTheme] = useState('light');

  return (
    <ThemeContext.Provider value={{ theme, setTheme }}>
      <ChildComponent />
    </ThemeContext.Provider>
  );
};

// Good
const ParentComponent = () => {
  const [theme, setTheme] = useState('light');

  return (
    <ThemeContext.Provider value={theme}>
      <ChildComponent setTheme={setTheme} />
    </ThemeContext.Provider>
  );
};
// Bad
const ThemeContext = React.createContext();

const ParentComponent = () => {
  const [theme, setTheme] = useState('light');

  return (
    <ThemeContext.Provider value={{ theme, setTheme }}>
      <ChildComponent />
    </ThemeContext.Provider>
  );
};

// Good
const ParentComponent = () => {
  const [theme, setTheme] = useState('light');

  return (
    <ThemeContext.Provider value={theme}>
      <ChildComponent setTheme={setTheme} />
    </ThemeContext.Provider>
  );
};
  1. 使用 Profiler:利用 React Profiler 来识别和优化性能瓶颈。
import { Profiler } from 'react';

const onRenderCallback = (
  id, // 发生提交的 Profiler 树的 "id"
  phase, // "mount" 或 "update"
  actualDuration, // 本次更新 committed 花费的渲染时间
  baseDuration, // 一个估计值,表示为渲染整颗子树而不使用 memoization 的情况下花费的时间
  startTime, // 本次更新中 React 开始渲染的时间
  commitTime, // 本次更新中 React committed 的时间
  interactions // 属于本次更新的 interactions 的集合
) => {
  // 处理或记录渲染时间……
}

<Profiler id="Navigation" onRender={onRenderCallback}>
  <Navigation {...props} />
</Profiler>
import { Profiler } from 'react';

const onRenderCallback = (
  id, // 发生提交的 Profiler 树的 "id"
  phase, // "mount" 或 "update"
  actualDuration, // 本次更新 committed 花费的渲染时间
  baseDuration, // 一个估计值,表示为渲染整颗子树而不使用 memoization 的情况下花费的时间
  startTime, // 本次更新中 React 开始渲染的时间
  commitTime, // 本次更新中 React committed 的时间
  interactions // 属于本次更新的 interactions 的集合
) => {
  // 处理或记录渲染时间……
}

<Profiler id="Navigation" onRender={onRenderCallback}>
  <Navigation {...props} />
</Profiler>
  1. 减少重排和重绘:优化 CSS 选择器,避免复杂选择器,优先使用 class 选择器。尽量使用 transformopacity 动画,而不是会触发重排的属性。
/* Bad */
div span {
  color: red;
}

/* Good */
.text {
  color: red;
}

/* Usage */
<div>
  <span className="text">Hello World</span>
</div>
/* Bad */
div span {
  color: red;
}

/* Good */
.text {
  color: red;
}

/* Usage */
<div>
  <span className="text">Hello World</span>
</div>

通过以上方法,可以有效优化 React 应用的性能,提升用户体验。