什么时候不应该使用反应备忘录?

我最近一直在玩 做出反应 16.6.0,我喜欢 反应备忘录的想法,但是我一直找不到任何关于最适合实现它的场景。

React 文档(https://reactjs.org/docs/react-api.html#reactmemo)似乎并没有暗示仅仅把它放在所有功能组件上会有什么影响。

因为它做了一个浅显的比较,以确定是否需要重新渲染,是否会出现 会对绩效产生负面影响吗?

这种情况似乎是执行的一个显而易见的选择:

// NameComponent.js
import React from "react";
const NameComponent = ({ name }) => <div>{name}</div>;
export default React.memo(NameComponent);


// CountComponent.js
import React from "react";
const CountComponent = ({ count }) => <div>{count}</div>;
export default CountComponent;


// App.js
import React from "react";
import NameComponent from "./NameComponent";
import CountComponent from "./CountComponent";


class App extends Component {
state = {
name: "Keith",
count: 0
};


handleClick = e => {
this.setState({ count: this.state.count + 1 });
};


render() {
return (
<div>
<NameComponent name={this.state.name} />
<CountComponent count={this.state.count} />
<button onClick={this.handleClick}>Add Count</button>
</div>
);
}
}

因为 name在这种情况下永远不会改变,所以对于 备忘录来说是有意义的。

但是,如果道具频繁更换,情况又会怎样呢?
如果我添加了另一个按钮,改变了状态中的其他内容并触发了重新呈现,那么在 备忘录中包装 CountComponent是否有意义,即使这个组件的设计就是为了频繁更新?

我想我的主要问题是 只要一切都保持纯粹,是否存在不用 React Memo 包装功能组件的情况?

65319 次浏览

我认为简短的回答是: 反应,备忘录对功能组件的作用就像 反应对类组件的作用一样。 从这个意义上说,当你使用备忘录时,它会评估那个函数组件的支持是否已经改变,如果改变了,那么它会执行函数的返回,否则它不会执行,避免了组件的重新呈现。

import React, { memo } from 'react';


const Component = () => {
debugger;
return (
<div>Hello World!</div>
);
};


const MemoComponent = memo(() => {
debugger;
return (
<div>Hello World!</div>
);
});

如果使用 Component作为更新的容器的子组件,那么每次父级更新时它都会重新呈现(每次都会触发调试器)。 另一方面,如果使用 MemoComponent,它将不会重新呈现(调试器只会在第一次呈现时触发)。

在这个例子中,发生这种情况是因为功能组件没有道具,如果它有道具,只有当道具发生变化时才会发生。

”请记住,传递给 useMemo 的函数在呈现期间运行。不要在渲染时做任何通常不会做的事情。例如,副作用属于 useEffect,而不是 useMemo。

您可以依赖 useMemo 作为性能优化,而不是语义保证。将来,React 可能会选择“忘记”一些先前制表的值,并在下次渲染时重新计算它们,例如为离屏组件释放内存。编写您的代码,使其在没有 useMemo 的情况下仍然可以工作ーー然后添加它以优化性能。(对于极少数不能重新计算值的情况,您可以延迟初始化 ref。)”

Https://reactjs.org/docs/hooks-faq.html#is-it-safe-to-omit-functions-from-the-list-of-dependencies

所有反应组件都实现 shouldComponentUpdate()方法。默认情况下(扩展 React.Component的组件) ,这始终返回 true。制表组件(通过 React.memo对函数组件或扩展 React.PureComponent对类组件)所引入的更改是 shouldComponentUpdate()方法的一个实现——它对状态和道具进行浅层比较。

看看组件生命周期方法上的 文件,在呈现发生之前,shouldComponentUpdate()就是调用的 一直都是,这意味着制表组件将在每次更新中包含这种附加的浅层比较。

考虑到这一点,制表组件 是的具有性能影响,这些影响的程度应该通过分析应用程序并确定是否使用制表工作得更好来确定。

为了回答你的问题,我不认为有一个明确的规则,当你应该或不应该制表组件,但是我认为同样的原则应该适用于决定是否应该覆盖 shouldComponentUpdate(): 通过建议的 分析工具找到性能问题,并确定是否需要优化一个组件。

同样的问题 有一个 Markerikson 在 React GitHub 问题追踪器上的回答。它得到了比这里的答案更多的赞扬。

我假设对于 React.memo和对于 shouldComponentUpdatePureComponent的一般建议是一样的: 进行比较的成本确实很小,而且有些情况下组件永远不会正确地制表(特别是如果它使用了 props.children)。所以,不要把所有东西都自动包装起来。查看您的应用程序在生产模式下的表现,使用 React 的分析构建和 DevTools 分析器来查看瓶颈在哪里,并战略性地使用这些工具来优化组件树中实际受益于这些优化的部分。

这个想法是为了避免使用制表,因为数据有可能经常变化。如博客中所述,这还包括回调,回调依赖于这种类型的数据。例如

<Foo onClick={() => handle(visitCount)}/>

我真的很喜欢这种简单的阅读。例子是伟大的

是否会出现对业绩产生负面影响的情况?

是的。如果所有组件都被 React.memo盲目地包装,那么性能可能会下降。

在许多情况下并不需要。要尝试使用性能关键组件,首先要做一些度量,添加制表,然后再次度量,看看增加的复杂性是否值得。

React.memo的成本是多少?

一个制表组件比较旧的新闻道具来决定,如果重新渲染-每个渲染周期
普通组件不关心,只是在父级中的道具/状态更改后呈现。

看一下 React shallowEqual实现,它是在 updateMemoComponent中调用的。

什么时候不使用 React memo

没有硬性规定,对 React.memo产生负面影响的事情:

  1. 组件经常重新渲染的道具,这已经改变了无论如何
  2. 组件是廉价的重新渲染
  3. 比较函数的执行代价很高

广告1: 在这种情况下,React.memo不能阻止重新渲染,但必须做额外的计算。
广告2: 就渲染、协调、 DOM 更改和副作用成本而言,对于一个“简单”组件来说,增加的比较成本是不值得的。
广告3: 道具越多,计算越多。你也可以传递一个更复杂的 自定义比较器

什么时候补充 React.memo

它只检查道具,不从内部改变上下文或状态。如果制表组件具有非基元 children,则 React.memo也是无用的。useMemo可以在这里补充 memo,比如:

// inside React.memo component
const ctxVal = useContext(MyContext); // context change normally trigger re-render
return useMemo(() => <Child />, [customDep]) // prevent re-render of children

您应该始终使用 React.memo LITERLY,因为比较组件返回的树总是比比较一对 props属性更昂贵

因此,不要听取任何人的意见,并将所有功能组件包装在 React.memo中。React.memo最初是打算内置在功能组件的核心部分,但由于缺少向下兼容,它在默认情况下不会使用。(因为它表面上比较对象,而且您可能正在使用组件中子对象的嵌套属性) =)

就是这样,这就是为什么 React 不使用备忘录的唯一原因。 =)

事实上,他们可以将版本17.0.0设置为 BREak 向下兼容,并将 React.memo设置为默认值,然后使用某种函数来取消这种行为,例如 React.deepProps =)

别听理论家的,伙计们 =)规则很简单:

如果您的组件使用深度比较道具,那么不要使用备忘录,否则总是使用它,比较两个对象总是比调用 React.createElement()和比较两个树、创建 FiberNodes 等要便宜。

理论家谈论他们自己不知道的,他们没有分析反应代码,他们不理解 FRP,他们不理解他们建议 =)

另外,如果组件使用的是 children道具,则 React.memo将无法工作,因为 children道具总是生成一个新的数组。但是最好不要为此操心,即使是这样的组件也应该包装在 React.memo中,因为计算资源是可以忽略不计的。

不应过度优化代码。一旦发现瓶颈,就应该优化代码。如果您没有注意到代码中的任何减速,那么就没有理由提前开始进行优化。因为这些优化是有代价的。优化不是免费的。优化函数需要消耗内存,存储这些函数也需要时间。React 开发人员还致力于使 React 更具性能。React 在引擎盖下有很多东西可以优化反应应用

最好在下列情况下使用 React.note () :

  • 组件呈现得太频繁,会降低应用程序的速度。

  • 组件渲染的成本很高(当加载时间超过100毫秒)。

  • 组件为同一组道具保持重新呈现。

可配置的嵌入式测试用例

我用一些参数设置了一个测试用例来度量同一组件的备忘录和非备忘录版本的平均呈现时间。您可以通过运行 下面的代码片段来尝试。它不能作为一个明确的答案,而是帮助演示不同的环境如何影响性能。

如果您只更改“带备忘录”复选框,它将保留平均时间,以便您可以对两者进行比较。更改任何其他设置将重置计数器。

注意,虽然输入元素添加的时间是固定的,但是它非常稳定,并不妨碍您观察参数的相对影响。如果你能使这个答案更加可靠/准确,你可以随时调整它。

对于备忘录版本,它还分别记录命中和未命中的平均时间。命中意味着备忘录可以跳过渲染。

您可以设置备忘录组件需要运行的频率(默认为每10次呈现一次)。

最后,假设一旦检测到任何差异,变化检测算法就会停止,我添加了一个参数来减少 更换道具的数量。您可以将其调高到100% ,使其只改变一个道具,或0% (默认) ,以改变所有道具。最后一个道具总是要换的。到目前为止,这似乎没有导致任何可衡量的变化。您可以忽略这个设置。

限制

道具是大小相等的简单字符串,这可能使比较操作比实际情况更容易。

一些时间信息是写在一个效果,以不“打扰”反应过度。因此,一些陈旧的时间信息保持显示,直到覆盖。

第一个渲染相当慢,你必须运行一些渲染之后取消它。

准确性

这并不意味着一个精确的测量,而是一种方法来比较行为的两个变体在其他类似的情况下。

虽然 App 组件有一些昂贵的逻辑,但是时间只能在那之后测量。我使用了一个效果来停止计时器,因为它几乎是第一件事反应后,它完成渲染,应该足够接近这个目的。

我的观察

我自己的测试结果证实了 当前最受欢迎的答案。附加检查的影响是微乎其微的,即使是荒谬的大量道具,和/或道具是大字符串。

如果对每次都会发生变化(变化间隔为1)的组件使用备忘录,那么它只能慢一定的速度。所以有一个周转点,你开始从中获益。

然而,我发现,即使有1/2的机会,组件将需要渲染,备忘录出来是有利的。

事实上,使用备忘录的影响是如此之小,以至于即使使用很多/很大的道具也很难观察到。

另一方面,即使对于简单的组件,跳过呈现所避免的时间也会显著增加。

您可以尝试使用不同的参数,检查总是比它避免的工作便宜得多。我没有发现任何配置,不使用备忘录是更快的平均... 或我有吗?

memo在极大的字符串 * 上运行较慢

* 如果组件不使用大字符串。

正当我准备提交答案时,我再次尝试将字符串增加到100万个字符。memo第一次陷入困境,而组件却没有。

enter image description here

即使只有1/10的“错过”,平均来说也明显慢下来了。

但是,如果您将这种大小的字符串作为道具传递,那么可能存在不止一个性能问题,而且这可能不是最大的问题。

而且,在极少数情况下,您确实需要传递它,它肯定会被组件使用。这可能会让它慢很多倍。目前,测试代码对这些大值没有任何操作。

let renderTimes = 0;
let compRenderTimes = 0;


function randomString(length) {
var result           = '';
var characters       = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
var charactersLength = characters.length;
for ( var i = 0; i < length; i++ ) {
result += characters.charAt(Math.floor(Math.random() *  charactersLength));
}
return result;
}


const {useState, useEffect, memo} = React;


let lastCompRenderStart;


function Comp (props) {
compRenderTimes++;
lastCompRenderStart = performance.now();
  

// Expensive task.
//console.log(props);
/*Object.entries(props).forEach(([k,v]) => {
if (`${k}${v}` === '4abc') {
alert('What a coincidence!');
}
});*/
  

useEffect(()=>{
const duration = performance.now() - lastCompRenderStart;
document.getElementById('lastCompRenderTime').innerHTML = duration.toFixed(2);
document.getElementById('nCompRenders').innerHTML = compRenderTimes;
});


return <p className="test">Testing {Object.keys(props).length} props, last rendered {performance.now()}</p>;
};


const MemoComp = memo(Comp);


let lastProps = {};
let lastRenderStart;
let lastWasHit = false;
let currentTotal = 0;
let memoRenders = 0;
let memoHitTotal = 0;
let memoHitRenders = 0;
let memoMissTotal = 0;
let memoMissRenders = 0;
let nomemoRenders = 0;
let nomemoTotal = 0;


function App() {
renderTimes++;
const [,refresh] = useState();
const [propAmount, setPropAmount] = useState(10);
const [propSize, setPropSize] = useState(10);
const [changeInterval, setChangeInterval] = useState(10);
const [changedPropOffset, setChangedPropOffset] = useState(0);
const [withMemo, setWithMemo] = useState(true);
  

useEffect(()=>{
    

renderTimes = 1;
compRenderTimes = 1;
currentTotal = 0;
memoRenders = 0;
memoHitTotal = 0;
memoHitRenders = 0;
memoMissTotal = 0;
memoMissRenders = 0;
nomemoRenders = 0;
nomemoTotal = 0;
}, [propAmount, propSize, changeInterval, changedPropOffset]);
  

let props = {};
lastWasHit = renderTimes !== 1 && renderTimes % changeInterval !== 0;
if (lastWasHit) {
// Reuse last props;
props = lastProps;
} else {
// Generate random new values after offset.
for (let i = 0; i < propAmount; i++) {
if (!!lastProps[i] && (i * 100 / propAmount < changedPropOffset) && i < propAmount - 1) {
props[i] = lastProps[i];
} else {
props[i] = randomString(propSize);
}
}
lastProps = props;
}


useEffect(()=>{
const duration = performance.now() - lastRenderStart;
document.getElementById('lastRenderTime').innerHTML = duration.toFixed(2);
    

if (!withMemo) {
nomemoRenders++;
nomemoTotal += duration;
document.getElementById('no-memo-renders').innerHTML = nomemoRenders;
document.getElementById('average-no-memo').innerHTML = (nomemoTotal / nomemoRenders).toFixed(2);


} else {
memoRenders++;
currentTotal += duration;
document.getElementById('memo-renders').innerHTML = memoRenders;
document.getElementById('average').innerHTML = (currentTotal / memoRenders).toFixed(2);


if (lastWasHit) {
memoHitRenders++;
memoHitTotal += duration;
document.getElementById('average-memo-hit').innerHTML = (memoHitTotal / memoHitRenders).toFixed(2);
} else {
memoMissRenders++;
document.getElementById('memo-renders-miss').innerHTML = memoMissRenders;


memoMissTotal += duration;
document.getElementById('average-memo-miss').innerHTML = (memoMissTotal / memoMissRenders).toFixed(2);
}
}
});


const ActualComp = withMemo ? MemoComp : Comp;
  

// This should give us the time needed for rendering the rest.
// I assume the settings inputs have has a more or less constant small impact on performance, at least while they're not being changed.
lastRenderStart = performance.now();


return <div>
<button onClick={() => refresh(renderTimes)} title='Trigger a render of App component'>render</button>
<input type='checkbox' onChange={event=>setWithMemo(!withMemo)} checked={withMemo}/>
<label  onClick={event=>setWithMemo(!withMemo)}>
with memo -
</label>
- Prop amount: <input type='number' title='How many props are passed to memoed component' value={propAmount} onChange={event=>setPropAmount(event.target.value)}/>
Prop size: <input type='number' title='How many random characters in prop value string?' value={propSize} onChange={event=>setPropSize(event.target.value)}/><br/>
Change interval: <input type='number' title='Change memoized component props every X renders.' value={changeInterval} onChange={event=>setChangeInterval(event.target.value)}/>
Changed prop offset <input type='number' min={0} max={100} step={10} title='Leave the first X percent of the props unchanged. To test if props comparison algorithm is affected by how fast it can find a difference. The last prop is always changed.' value={changedPropOffset} onChange={event=>setChangedPropOffset(event.target.value)}/>
<ActualComp {...props} />
</div>;
};




ReactDOM.render(<App/>, document.getElementById('root'));
#lastRenderTime {
background: yellow;
}


#lastCompRenderTime {
background: lightblue;
}


.test {
background: lightgrey;
border-radius: 4px;
}


td {
border: 1px solid lightgrey;
padding: 4px;
}
 

input[type=number] {
max-width: 72px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.2/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.2/umd/react-dom.production.min.js"></script>


<div id="root"></div>


<table>
<th>
<td>N renders</td>
<td>Average duration</td>
<td>Memo (hit) duration</td>
<td>Memo (miss) duration</td>
</th>
<tr>
<tr>
<td>No memo</td>
<td><span id="no-memo-renders"></span></td>
<td><span id="average-no-memo"></span></td>
</tr>




<tr>
<td>memo</td>
<td><span id="memo-renders"></span>, <span id="memo-renders-miss"></span> miss</td>
<td><span id="average"></span></td>
<td><span id="average-memo-hit"></span></td>
<td><span id="average-memo-miss"></span></td>
</tr>
</table>
=====
<table>
<tr>
<td>Last rendered App</td>
<td><span id="lastRenderTime"></span></td>
</tr>
<tr>
<td>Component renders</td>
<td><span id="nCompRenders"></span></td>
</tr>
<tr>
<td>Last rendered Component</td>
<td><span id="lastCompRenderTime"></span></td>
</tr>
</table>