在你memo()之前

86 阅读4分钟

很多文章都写过 React 的性能优化,一般来说,当你的 state 更新缓慢的时候,你需要

  1. 确认是否在生产环境(开发环境通常具有开发相关的依赖,有时候会慢一个数量级)
  2. 确认是否把 state 放在一棵复杂结构并且在高位的树。(例如把 Input state 放在一棵树上并不是一个好主意)
  3. 使用 React DevTools 分析器查看重渲染过程,在性能消耗最多的子树使用 memo() 包裹。

最后一步非常的烦人,尤其是组件在中间的时候,在未来,这一步可能可以让编译器自动解决。

在这篇文章中,我想分享两个不同并且非常简单的优化渲染性能的方案。

这些方法你一定知道,它们没打算取代 memo() 或者 useMemo() 的地位,但是非常值得优先考虑。

一个很慢的组件

这里有一个有非常严重的渲染性能的组件

import { useState } from 'react'; 
export default function App() {
  let [color, setColor] = useState('red');
  return ( <div> 
    <input value={color} onChange={(e) => setColor(e.target.value)} /> 
    <p style={{ color }}>Hello, world!</p> 
    <ExpensiveTree />
  </div>);
}
function ExpensiveTree() { 
  let now = performance.now(); 
  while (performance.now() - now < 100) { 
    // Artificial delay -- do nothing for 100ms 
  } 
  return <p>I am a very slow component tree.</p>;
}

这个组件的问题是 color 在 App 里改变的时候 , 重渲染 <ExpensiveTree /> 组件会非常的慢

我可以给他包裹上 memo() 然后这个组件就会被缓存起来(因为他的state并没有改变),这种方案已经很成熟,所以我不打算花时间在它身上。下面给出两种方案。

方案1:下移 state

如果你仔细看了这个示例代码, 你会发现,只有使用了 color 变量的地方才是需要重渲染的地方

export default function App() { 
  let [color, setColor] = useState('red'); 
    return (<div> 
      <input value={color} onChange={(e) => setColor(e.target.value)} /> 
      <p style={{ color }}>Hello, world!</p> 
      <ExpensiveTree />  // 这一行与 `color` 无关
    </div>);
}

所以,我们可以把 color 影响到的部分抽取出来,放到 Form 组件里面

export default function App() { 
  return ( <>
    <Form /> 
    <ExpensiveTree />
  </> );
} 
 function Form() { 
   let [color, setColor] = useState('red'); 
   return ( <> 
     <input value={color} onChange={(e) => setColor(e.target.value)} /> 
     <p style={{ color }}>Hello, world!</p> 
   </> );
 }

现在,当 color 改变的时候, <ExpensiveTree /> 并不会重渲染

方案2:提升内容

上面的方法在父元素都使用 color 变量时不生效,例如,我们把 color 也传递给 div

export default function App() { 
  let [color, setColor] = useState('red'); 
  return ( <div style={{ color }}> 
    <input value={color} onChange={(e) => setColor(e.target.value)} /> 
    <p>Hello, world!</p> 
    <ExpensiveTree /> 
  </div> );
}

这样我们无法下移 state 了,因为父元素也使用了 color, 看起来我们无法避免要使用 memo() 了?

或者我们可以使用别的?

答案非常简单

export default function App() { 
  return ( <ColorPicker> 
    <p>Hello, world!</p> 
    <ExpensiveTree /> 
  </ColorPicker> );
} 
function ColorPicker({ children }) { 
  let [color, setColor] = useState("red"); 
  return (<div style={{ color }}> 
    <input value={color} onChange={(e) => setColor(e.target.value)} />
    {children} 
  </div> );
}

我们分割了组件APP为两个部分,其中一个部分取决于 color 影响的范围,把该范围内的都抽取到 <ColorPicker />

另一部分不在color范围内的内容我们作为children 传递给<ColorPicker />

color改变的时候,<ColorPicker />会重渲染,但是他的 children 依然是不变的,React 会忽略这部分子树的渲染过程,采用上一次渲染中的内容去填充

结果就是 <ExpensiveTree /> 不会被重渲染

启示

在你应用 memo 或者 useMemo,之前,可能需要检查一下,看下你是否可以把组件分割成 state 影响的部分和state忽略的部分

有趣的是,这些方法实际上都并没有对性能优化做过什么操作,使用传递children 这样的方式分割组件, 通常都会使你的应用的数据流更容易跟踪,并减少通过树向下传递的属性数量。这种情况下,性能优化只是顺带的,并不是我们的目的

奇怪的是,这种模式还会给未来解锁更多的性能好处

例如, 当服务端的组件被采用的时候,ColorPicker 组件可以把服务端的组件作为 children。无论是整个 <ExpensiveTree /> 组件或是运行在服务端的一部分,就算是顶层的 state 更新, 也会跳过相应的部分渲染。

这就算是memo() 也是做不到的,重申一下,两个方案是相辅相成的,不要忽视状态下移,还有内容提升。

这不是一个新想法。它是 React 组合模型的自然结果。它很简单,以至于被低估了,值得更多一点的关注。

原文地址