【问题标题】:When should you NOT use React memo?什么时候不应该使用 React 备忘录?
【发布时间】:2019-04-04 02:23:38
【问题描述】:

我最近一直在玩 React 16.6.0 并且我喜欢 React Memo 的想法,但我一直无法找到最好的场景适合实现它。

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 在这种情况下永远不会改变,所以memoize 是有意义的。

但是道具经常更换的情况呢?
如果我添加了另一个按钮来更改状态中的其他内容并触发重新渲染,那么将 CountComponent 包装在 memo 中是否有意义,即使此组件的设计目的是要经常更新?

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

【问题讨论】:

  • 如果你的组件总是重新渲染,它每次都会做一个不必要的浅 prop 检查。和 PureComponent 一样。
  • @AndyRay 我想我对收益递减的阈值在哪里感兴趣。如果我的功能组件理论上需要准确地重新渲染 95% 的时间,那么重新渲染的成本是否需要使用备忘录?我知道这是一个超级具体的场景,我并不是在寻找确切的基准,我只是更好奇是否有一条可以在沙子上画的线。
  • 我怀疑是否有人研究过该指标,如果 memoization 调用是您的瓶颈,那么可能有一些非常特定于您的应用需求的东西很难给出一般性建议。
  • 也许一个更尖锐的问题是:“你什么时候不应该使用 React 备忘录?”。或者“你是否应该一直使用 React 备忘录并仅在出现性能问题时才选择退出?”。
  • @protoEvangelion 好建议,我要更新问题标题

标签: javascript reactjs


【解决方案1】:

我认为简短的回答是:React.memo 对功能组件的作用类似于 React.PureComponent 对类组件的作用。 从这个意义上说,当你使用 memo 时,它会评估该功能组件的 props 是否发生了变化,如果是,那么它将执行函数的返回,否则它不会,避免重新渲染组件。

import React, { memo } from 'react';

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

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

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

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

【讨论】:

  • 这没有回答问题,它清楚地询问了何时使用备忘录,而不是“什么是备忘录”,这是一个不同的问题,你已经回答了.
  • @vsync 我想通过解释它是什么,就可以清楚地了解何时使用或不使用(这最终是开发人员的选择)。在我看来,我认为它几乎应该一直使用,老实说,我想不出任何我建议不要使用它的具体情况。
  • 如果确实应该一直使用它并且你想不出任何用例,那么这应该是默认设置,而编写 React 的聪明人选择了其他方式,这意味着有一个很好的理由... Here's a tweet 来自 Dan Abramov
  • @vsync 我认为您应该再次阅读我的 cmets,因为我认为您误解了。首先,我说“应该几乎一直使用”(关键字几乎)。其次,当我说我想不出一个建议不要使用它的情况时,这是我个人和诚实的意见,如果你自己有这种情况,那么你应该分享它:)
  • 第三,回到我最初的答案,我试图解释它是什么和不是什么,这就是最终由开发人员判断是否使用它的原因。何时使用 lodash,例如...何时使用扩展 PureComponent 而不是 Component。一切最终都有成本,但根据我的观点和个人经验,我并没有因为成本而无法在我的组件上使用备忘录而不是使用它的好处。
【解决方案2】:

“请记住,传递给 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

【讨论】:

  • 与问题完全无关。你的答案是指useMemo 钩子 notreact.memo 函数混淆
【解决方案3】:

所有的 react 组件都实现了shouldComponentUpdate() 方法。默认情况下(扩展 React.Component 的组件),它总是返回 true。记忆组件(通过React.memo 用于功能组件或扩展React.PureComponent 用于类组件)引入的更改是shouldComponentUpdate() 方法的实现 - 它对状态和道具进行浅层比较。

查看组件生命周期方法上的documentationshouldComponentUpdate()总是在渲染发生之前调用,这意味着记忆组件将在每次更新时包含这个额外的浅比较。

考虑到这一点,记忆组件确实会对性能产生影响,而这些影响的大小应通过分析您的应用程序并确定它是否在有记忆或没有记忆的情况下效果更好来确定。 p>

为了回答您的问题,我认为您应该或不应该记忆组件时没有明确的规则,但是我认为应该应用与决定是否应该覆盖 shouldComponentUpdate() 时相同的原则:find performance通过建议的profiling tools 解决问题并确定您是否需要优化组件。

【讨论】:

  • 这是一个很好的答案,我认为你是绝对正确的,分析是确定性能缺陷大小的必要工具。
  • 是的,我认为 React 为我们提供的许多功能并不意味着是魔杖,而是在某些(有时是一般)情况下帮助解决问题的更多工具,我很高兴你找到了我的回答有用的@KeithBrewster
  • 你应该提到内存影响(内存不是性能)。我不知道为什么人们认为React.memo 的成本只是一个非常肤浅的比较。当它实际上是“额外的浅比较”+“每个组件实例的额外渲染快照”时。至少对于功能组件而言。
  • @IvanKleshnin 有趣的一点!我有兴趣详细说明第二个语句“每个组件实例的额外渲染快照”
  • @NickMitchell 我的意思是React.memo 内部将 prop 值存储在内存中(以便能够与新的 prop 值进行比较)。如果该数据足够大,它可以有一个可测量的足迹。
【解决方案4】:

The same question 有一个answer by markerikson on the React GitHub issue tracker。它得到的赞许比这里的答案多。

我认为对于React.memoshouldComponentUpdatePureComponent 应用相同的一般建议:进行比较确实需要很小的成本,并且在某些情况下组件永远不会正确记忆(尤其是如果它使用props.children)。所以,不要只是在任何地方自动包装所有东西。查看您的应用在生产模式下的行为方式,使用 React 的分析构建和 DevTools 分析器来查看瓶颈所在,并战略性地使用这些工具来优化组件树中真正受益于这些优化的部分。

【讨论】:

    【解决方案5】:

    我们的想法是避免使用记忆化,因为数据可能会经常变化。如博客中所述,这还包括依赖于此类数据类型的回调。例如

    等函数

    &lt;Foo onClick={() =&gt; handle(visitCount)}/&gt;

    我真的很喜欢这种简单的阅读。这些例子很棒。 https://dmitripavlutin.com/use-react-memo-wisely/

    【讨论】:

      【解决方案6】:

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

      是的。如果所有组件都被 React.memo 无意识地包装,您最终可能会得到更差的性能。

      在很多情况下不需要。要尝试使用性能关键组件,请先进行一些测量,添加记忆,然后再次测量以查看增加的复杂性是否值得。

      React.memo 的费用是多少?

      记忆化组件将旧的与新闻道具进行比较以决定是否重新渲染 - 每个渲染周期
      一个普通的组件不关心,只是在父组件的 props/state 改变之后渲染。

      看看 React shallowEqual 的实现,它在updateMemoComponent 中被调用。

      什么时候不使用React memo

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

      1. 组件经常使用 props 重新渲染,但无论如何都已更改
      2. 组件重新渲染成本低
      3. 比较函数的执行成本很高

      广告 1:在这种情况下,React.memo 无法阻止重新渲染,但必须进行额外的计算。
      广告 2:在渲染、协调、DOM 更改和副作用成本方面,对于“简单”组件而言,增加比较成本是不值得的。
      广告3:道具越多,计算越多。你也可以传入更复杂的custom comparer

      什么时候补React.memo?

      它只检查道具,而不是从内部检查上下文变化或状态变化。 React.memo 也没有用,如果记忆的组件有非原始的childrenuseMemo 可以在这里补充memo,比如:

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

      【讨论】:

      • 如何重新渲染整个 HTML 组件比字面比较值更便宜,可以说是每种编程语言中最快的操作之一?这在很多层面上都是错误的。虽然盲目记忆组件确实可能需要更多的 cpu 周期,但它永远无法与任何重新渲染相比,除非你一次真正地比较数千个参数,这是不太可能的。
      • 好吧,我的主要观点是:在大多数情况下,如果您的组件在父级重新渲染的情况下无论如何都更改了 props,React.memo 必须比普通组件做更多的工作(协调 道具差异)。我个人也不是premature optimization 的粉丝——这些变化总是有代价的(比如增加抽象/复杂性的代价),应该是合理的。
      • 另外有趣的是:在链接的文章中,worse performance 在每个组件上都添加了PureComponent aka React.memo 的功能后,已经被体验和衡量了。
      • 您的大部分观点都是正确的,除了重新渲染的廉价性之外,我同意一切。显然,在一个总是重新渲染的组件上,记忆是没有用的,而且对性能不利。我只是相信 react 和 react-kind 框架的美妙之处在于虚拟 dom 的概念,它的主要目的是防止不必要的昂贵的真实 DOM 操作。所以这是框架的重点。肤浅的比较并不适合所有场景,但对于许多场景来说仍然是一个好主意。
      【解决方案7】:

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

      所以不要听任何人的话,将所有功能组件包装在React.memo 中。 React.memo原本打算内置到功能组件的核心,但由于失去了向后兼容性,默认不使用。 (因为它从表面上比较对象,并且您可能正在使用组件中子对象的嵌套属性)=)

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

      事实上,他们可以制作版本 17.0.0,这会破坏向后兼容性,并将 React.memo 设为默认值,并制作某种函数来取消此行为,例如 React.deepProps =)

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

      如果您的组件使用 DEEP COMPARING PROPS,则不要使用备忘录,否则总是使用它,比较两个对象总是比调用 React.createElement() 并比较两棵树、创建 FiberNode 等便宜。

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

      附:如果你的组件使用children prop,React.memo 将不起作用,因为children prop 总是创建一个新数组。但是最好不要在意这个,即使是这样的组件也应该被包裹在React.memo中,因为计算资源可以忽略不计。

      【讨论】:

      • 对此有何反驳?
      • @anonym 认为 React.memo 为 useEffect (Component, [depending ...]) 仅在 useEffect 的情况下,文档最初声明数组值是表面上比较的 =),并且在props 的情况,这个没有说明,这就是 React.memo 默认不启用的原因 =) 顺便说一句,我是 react 的开发者之一。
      • @anonym 我们的想法是 React.memo 不会停止渲染它的子节点,但也会检查子节点的 props 变化。现在 React.memo 阻塞​​了它整个树分支的渲染,这也是默认不启用的原因之一。诀窍在于反应性具有“反应性颗粒”这样的概念。每个grain都有依赖关系。比较依赖关系,如果它们没有改变,则不会重新计算粒度。这些是 FRP 中的标准优化(React 是 FRP 实现之一)
      • @anonym 我相信唯一需要注意的是,如果你的道具记忆力很重。记忆意味着可能永远将道具存储在内存中,即直到下一次重新渲染,新的可能需要存储大量内存的道具。只有当组件被卸载时,该内存才会被释放。这就是您在速度和内存之间取得的平衡。
      • 这个答案完全是错误的和误导性的。实际上很容易想象 props 的比较会比 VDOM 的比较慢的情况。请记住,VDOM 树是一个普通的 JS 数据。现在这一切都归结为道具和 VDOM 树的大小之间的差异。如果您的组件返回一个相对较大的 VDOM 并接受占用空间相对较小的 props(常见场景),它将受益于 React.memo。如果你的组件返回一个相对较小的 VDOM 并且 props 相对较大......比较 VDOM 会更快,避免额外的 props 比较。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2012-09-22
      • 1970-01-01
      • 2023-04-02
      • 2011-04-15
      • 2017-04-10
      • 2012-03-19
      • 2018-05-12
      相关资源
      最近更新 更多