扣丁书屋

【React】1138- React Hooks 性能优化的正确姿势

前言

React Hooks 出来很长一段时间了,相信有不少的朋友已经深度使用了。无论是 React 本身,还是其生态中,都在摸索着进步。鉴于我使用的 React 的经验,给大家分享一下。对于 React hooks,性能优化可以从以下几个方面着手考虑。

场景1

在使用了 React Hooks 后,很多人都会抱怨渲染的次数变多了。没错,官方就是这么推荐的:

我们推荐把 state 切分成多个 state 变量,每个变量包含的不同值会在同时发生变化。

function Box() {
const [position, setPosition] = useState({ left: 0, top: 0 });
const [size, setSize] = useState({ width: 100, height: 100 });
// ...
}

这种写法在异步的条件下,比如调用接口返回后,同时 setPositionsetSize,相较于 class 写法会额外多出一次 render。这也就是渲染次数变多的根本原因。当然这种写法仍然值得推荐,可读性和可维护性更高,能更好的逻辑分离。

针对这种场景若出现十几或几十个 useState 的时候,可读性就会变差,这个时候就需要相关性的组件化了。以逻辑为导向,抽离在不同的文件中,借助 React.memo 来屏蔽其他 state 导致的 rerender

const Position = React.memo(({ position }: PositionProps) => {
  // position 相关逻辑
  return (
    <div>{position.left}</div>
  );
});

因此在 React hooks 组件中尽量不要写流水线代码,保持在 200 行左右最佳,通过组件化降低耦合和复杂度,还能优化一定的性能。

场景2

class 对比 hooks,上代码:

class Counter extends React.Component {
  state = {
    count: 0,
  };
  increment = () => {
    this.setState((prev) => ({
      count: prev.count + 1,
    }));
  };
  render() {
    const { count } = this.state;
    return <ChildComponent count={count} onClick={this.increment} />;
  }
}

function Counter() {
  const [count, setCount] = React.useState(0);
  function increment() {
    setCount((n) => n + 1);
  }
  return <ChildComponent count={count} onClick={increment} />;
}

凭直观感受,你是否会觉得 hooks 等同于 class 的写法?错,hooks 的写法已经埋了一个坑。在 count 状态更新的时候, Counter 组件会重新执行,这个时候会重新创建一个新的函数 increment。这样传递给 ChildComponentonClick 每次都是一个新的函数,从而导致 ChildComponent 组件的 React.memo 失效。

解决办法:

function usePersistFn<T extends (...args: any[]) => any>(fn: T) {
  const ref = React.useRef<Function>(() => {
    throw new Error('Cannot call function while rendering.');
  });
  ref.current = fn;
  return React.useCallback(ref.current as T, [ref]);
}

// 建议使用 `usePersistFn`
const increment = usePersistFn(() => {
  setCount((n) => n + 1);
});
// 或者使用 useCallback
const increment = React.useCallback(() => {
  setCount((n) => n + 1);
}, []);

上面声明了 usePersistFn 自定义 hook,可以保证函数地址在本组件中永远不会变化。完美解决 useCallback 依赖值变化而重新生成新函数的问题,逻辑量大的组件强烈建议使用。

不仅仅是函数,比如每次 render 所创建的新对象,传递给子组件都会有此类问题。尽量不在组件的参数上传递因 render 而创建的对象,比如 style={{ width: 0 }} 此类的代码用 React.useMemoReact.memo 编写 equal 函数来优化。

style 若不需改变,可以提取到组件外面声明。尽管这样做写法感觉太繁琐,但是不依赖 React.memo 重新实现的情况下,是优化性能的有效手段。


const style: React.CSSProperties = { width: 100 };
function CustomComponent() {
  return <ChildComponent style={style} />;
}

场景3

对于复杂的场景,使用 useWhyDidYouUpdate hook 来调试当前的可变变量引起的 rerender。这个函数也可直接使用 ahooks 中的实现。

function useWhyDidYouUpdate(name, props) {
  const previousProps = useRef();
  useEffect(() => {
    if (previousProps.current) {
      const allKeys = Object.keys({ ...previousProps.current, ...props });
      const changesObj = {};
      allKeys.forEach(key => {
        if (previousProps.current[key] !== props[key]) {
          changesObj[key] = {
            from: previousProps.current[key],
            to: props[key]
          };
        }
      });
      if (Object.keys(changesObj).length) {
        console.log('[why-did-you-update]', name, changesObj);
      }
    }
    previousProps.current = props;
  });
}
const Counter = React.memo(props => {
  useWhyDidYouUpdate('Counter', props);
  return <div style={props.style}>{props.count}</div>;
});

useWhyDidYouUpdate 中所监听的 props 发生了变化,则会打印对应的值对比,是调试中的神器,极力推荐。

场景4

借助 Chrome Performance 代码进行调试,录制一段操作,在 Timings 选项卡中分析耗时最长逻辑在什么地方,会展现出组件的层级栈,然后精准优化。

场景5

React 中是极力推荐函数式编程,可以让数据不可变性作为我们优化的手段。我在 React class 时代大量使用了 immutable.js 结合 redux 来搭建业务,与 ReactPureComponnet 完美配合,性能保持非常好。但是在 React hooks 中再结合 typescript 它就显得有点格格不入了,类型支持得不是很完美。这里可以尝试一下 immer.js,引入成本小,写法也简洁了不少。

const nextState = produce(currentState, (draft) => {
  draft.p.x.push(2);
})
 // true
currentState === nextState;

场景6

复杂场景使用 Map 对象代替数组操作,map.get(), map.has(),与数组查找相比尤其高效。


// Map
const map = new Map([['a', { id: 'a' }], ['b', { id: 'b' }], ['c', { id: 'c' }]]);
// 查找值
map.has('a');
// 获取值
map.get('a');
// 遍历
map.forEach(n => n);
// 它可以很容易转换为数组
Array.from(map.values());
// 数组
const list = [{ id: 'a' }, { id: 'b' }, { id: 'c' }];
// 查找值
list.some(n => n.id === 'a');
// 获取值
list.find(n => n.id === 'a');
// 遍历
list.forEach(n => n);

结语

React 性能调优,除了阻止 rerender,还有与写代码的方式有关系。最后,我要推一下近期写的 React 状态管理库 https://github.com/MinJieLiu/heo,也可以作为性能优化的一个手段,希望大家从 redux 的繁琐中解放出来,省下的时间用来享受生活


https://mp.weixin.qq.com/s?__biz=MjM5MDc4MzgxNA==&mid=2458464447&idx=2&sn=23bc6b13fb9a8076ff0310d9b603c04b&chksm=b1c2109686b599800b353ecefc35d9562abe715fb4b9ebb60ed7a853be4d3d4d44ce03c47b47&mpshare=1&scene=1&srcid=1110d8vQMlSRrEM3jtN4hwrz&sharer_sharetime=1636524798616&sharer_shareid=7f2af9604fbdc84c4e358e57e738e84a#rd

深入 React 函数组件的 re-render 原理及优化

发布于:8月以前  |  328次阅读  |  详细内容 »

手写系列-实现一个铂金段位的 React

发布于:8月以前  |  273次阅读  |  详细内容 »

如何写出更优雅的 React 组件 - 代码结构篇

发布于:8月以前  |  309次阅读  |  详细内容 »

React 精品组件:mac-scrollbar

发布于:8月以前  |  299次阅读  |  详细内容 »

React 精品组件:mac-scrollbar

发布于:8月以前  |  316次阅读  |  详细内容 »

React 18 Beta 来了

发布于:8月以前  |  385次阅读  |  详细内容 »

手把手教你写一个 React 状态管理库

发布于:8月以前  |  306次阅读  |  详细内容 »

如何将 React 应用程序加载时间减少 70%

发布于:8月以前  |  327次阅读  |  详细内容 »

React 18 新特性之 startTransition

发布于:8月以前  |  334次阅读  |  详细内容 »

Reaction Engines成立新合资公司以打造紧凑、轻质的氨反应器

英国的Reaction Engines公司已经宣布成立一家合资企业以打造紧凑、轻质的氨反应器。这家公司表示,这种反应器可以用于航运和离网能源发电等困难行业的脱碳,并且还可以用于航空。

发布于:9月以前  |  381次阅读  |  详细内容 »

你不知道的 React 最佳实践

发布于:9月以前  |  285次阅读  |  详细内容 »

这才是 React Hooks 性能优化的正确姿势

发布于:9月以前  |  315次阅读  |  详细内容 »

所属标签

最多阅读

Android插件化方案 3年以前  |  232669次阅读
SourceTree,让你忘掉Git命令的工具 3年以前  |  3513次阅读
朴素贝叶斯分类器的应用 3年以前  |  3202次阅读
用Sublime打造Protobuf迷你IDE 3年以前  |  3083次阅读
在Python的Django框架中包装视图函数 3年以前  |  3060次阅读
Python贪吃蛇游戏编写代码 3年以前  |  3056次阅读
在Sublime中高亮显示Proto Buffer 3年以前  |  3039次阅读
Google Shell 风格指南 3年以前  |  2937次阅读
Google Python 风格指南 3年以前  |  2905次阅读
Genymotion下载及安装使用 3年以前  |  2830次阅读
Markdown语法 转义 3年以前  |  2821次阅读
Markdown语法 链接 3年以前  |  2695次阅读
Markdown语法 段落内代码 3年以前  |  2629次阅读
java中NIO与传统IO 3年以前  |  2589次阅读

手机扫码阅读