React Memo不是你优化的第一选择(一)

简介: React Memo不是你优化的第一选择(一)

生活就是当你忙着制定其他计划时,发生在你身上的事情 - 约翰·列侬

大家好,我是柒八九

前言

Dan的文章在使用React.memo之前的注意事项中,通过几个例子来描述,有时候我们可以通过组件组合的方式来优化组件的多余渲染。文章中提到要么通过将下放State,要么将内容提升。因为组件组合是React的自然思维模式。正如Dan所指出的,这也将与Server Components非常契合。

image.png

然后,在各种文章中,都提倡克制useMemo的使用,优先使用组件组合来处理组件冗余渲染的问题。但是,它们都没讲明白,遇到这些问题,为什么不首选使用React.memo呢?

最核心的点,就是

Memo很容易被破坏

下面,我们就由浅入深的来窥探其中的门道。

好了,天不早了,干点正事哇。


你能所学到的知识点

  1. 前置知识点
  2. 问题复现
  3. children
  4. 替代方案
  5. 问题的根源

前置知识点

前置知识点,只是做一个概念的介绍,不会做深度解释。因为,这些概念在下面文章中会有出现,为了让行文更加的顺畅,所以将本该在文内的概念解释放到前面来。如果大家对这些概念熟悉,可以直接忽略


同时,由于阅读我文章的群体有很多,所以有些知识点可能我视之若珍宝,尔视只如草芥,弃之如敝履。以下知识点,请酌情使用

Object.is

Object.isJavaScript 中的一个内建函数,用于比较两个值是否严格相等。它的作用类似于严格相等操作符 ===,但有一些关键区别。

语法

Object.is(value1, value2)

参数

  • value1:比较的第一个值。
  • value2:比较的第二个值。

返回值

Object.is 返回一个布尔值,表示两个值是否严格相等

特点

  1. NaN 相等性:Object.is 在比较 NaN 值时与其他方法不同。它认为 Object.is(NaN, NaN)true,而严格相等操作符 === 认为 NaN === NaNfalse
Object.is(NaN, NaN); // true
NaN === NaN; // false
  1. +0 和 -0 不相等:Object.is 能够区分正零负零,即 Object.is(+0, -0) 返回 false,而 === 会认为它们相等。
Object.is(+0, -0); // false
+0 === -0; // true
  1. +0 和 -0 与0相等: 除了自身之外,正零和负零都与其他数字相等。
Object.is(+0, 0); // true
Object.is(-0, 0); // true
  1. 其它值的比较: 对于其他值,Object.is 表现与 === 相同。
Object.is(1, 1); // true
Object.is('foo', 'foo'); // true

用途

Object.is 主要用于比较两个值,特别是在需要明确处理 NaN 或区分正零和负零时。这可以用于创建更精确的相等性检查,而不受 JavaScript 中一些奇怪的行为的影响。例如,当比较浮点数或需要区分 NaN 时,Object.is 可能更有用。

function areTheyEqual(value1, value2) {
  return Object.is(value1, value2);
}
areTheyEqual(NaN, NaN); // true
areTheyEqual(+0, -0); // false

Record 和Tuple

它们属于ECMAScript提案Records and Tuples

  • Record(记录):这将是一种深度不可变类对象结构,与普通JavaScript对象不同,其属性和值将是不可变的。这将有助于避免对象的属性被无意中更改。
  • Tuple(元组):这将是一种深度不可变类数组结构,与普通JavaScript数组不同,其元素将是不可变的。这将有助于确保元组的内容在创建后不可更改。

这些看起来类似于普通的对象和数组,但它们具有以“#”前缀为特征:

const record = #{a: 1, b: 2};
record.a;
// 1
const updatedRecord = #{...record, b: 3};
// #{a: 1, b: 3};
const tuple = #[1, 5, 2, 3, 4];
tuple[1];
// 5
const filteredTuple = tuple.filter(num => num > 2);
// #[5, 3, 4];

它们默认是深度不可变的:

const record = #{a: 1, b: 2};
record.b = 3;
// 抛出 TypeError

它们可以被视为复合基本类型,并且可以通过值进行比较。

非常重要:两个深度相等的Record将始终使用 === 运算符返回 true

{a: 1, b: [3, 4]} === {a: 1, b: [3, 4]}
// 使用对象 => false
#{a: 1, b: #[3, 4]} === #{a: 1, b: #[3, 4]}
// 使用记录 => true

我们可以认为Record的变量就是其实际值,类似于常规JS原始类型。

它们与JSON互操作:

const record = JSON.parseImmutable('{a: 1, b: [2, 3]}');
// #{a: 1, b: #[2, 3]}
JSON.stringify(record);
// '{a: 1, b: [2, 3]}'

它们只能包含其他RecordTuple,或简单数据类型。

const record1 = #{
  a: {
    regular: 'object'
  },
};
// 抛出 TypeError,因为记录不能包含对象
const record2 = #{
  b: new Date(),
};
// 抛出 TypeError,因为记录不能包含日期
const record3 = #{
  c: new MyClass(),
};
// 抛出 TypeError,因为记录不能包含类
const record4 = #{
  d: function () {
    alert('forbidden');
  },
};
// 抛出 TypeError,因为记录不能包含函数

2. 问题复现

上面提到了 -Memo很容易被破坏

简而言之:当React渲染一个组件树时,它会从上往下渲染所有子组件。一旦渲染开始,我们就没有办法停止它。通常情况下,这是一件好事,因为渲染确保我们在屏幕上看到正确的状态反映。此外,渲染通常是快速的。

当然还有那些特殊情况,它们需要处理一下耗时任务,从而使的渲染变得步履蹒跚。同时,由于某些原因,我们都有一些组件(前任留下的💩⛰️,或者核心业务),我们无法轻易改变它们,与此同时它们的渲染速度还不尽人意。而此时,小可爱产品,又提出了优化需求。而我们就不得不赶鸭子上架。

幸运的是,React内置机制中存在优化策略,那就是

在渲染时候,当它发现此次需要渲染的东西和之前是相同的,它是选择使用之前的结果。

假设,我们有如下的组件。

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, 789!</p>
      <ExpensiveComponent />
    </div>
  );
}
function ExpensiveComponent() {
  let now = performance.now();
  while (performance.now() - now < 100) {
    // 手动模拟,耗时任务 -- 此处会卡顿100ms
  }
  // 打印被渲染的次数
  console.log('我被渲染了');
  return <p>耗时渲染</p>;
}

我们可以将上面的代码,放置在任何线上环境进行测试。运行后我们就会发现,当App中的color变化时,会重新渲染一次被我们人为延缓渲染的<ExpensiveComponent />组件。

在实际开发中,如果ExpensiveComponent渲染需要很长时间,那这个部分就会很引起性能崩塌。

这是我们之前写的关于如何测试浏览器性能的文章,然后大家可以按需获取。

  1. 浏览器之性能指标_FCP
  2. 浏览器之性能指标-LCP
  3. 浏览器之性能指标-CLS
  4. 浏览器之性能指标-FID
  5. 浏览器之性能指标-TTI
  6. 浏览器之性能指标-TBT
  7. 浏览器之性能指标-INP

下面,我们就来解决上面出现的问题。

解法 1: 下放State

如果我们仔细看一下上面的问题代码,我们会注意到返回的组件树中只有一部分真正关心当前的color。而<ExpensiveComponent/>却对这些信息充耳不闻

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, 789!</p>
      <ExpensiveComponent />
    </div>
  );
}
function ExpensiveComponent() {
  let now = performance.now();
  while (performance.now() - now < 100) {
    // 手动模拟,耗时任务 -- 此处会卡顿100ms
  }
  // 打印被渲染的次数
  console.log('我被渲染了');
  return <p>耗时渲染</p>;
}

我们把关心color部分提取到Form组件中然后将state移动到该组件里:

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

如果color变化了,只有Form会重新渲染。将state下放到真正关心的组件中,这样就可以完美避开渲染污染。

动物世界看过哇。我们可以认为,这个是生殖隔离。虽然,有马和驴生下骡子的特例,但是骡子无法生育,就算存在污染,那也是限制在有限范围内。而不会出现子子孙孙无穷匮也的情况。

image.png

相关文章
|
2月前
|
前端开发 JavaScript UED
使用React Hooks优化前端应用性能
本文将深入探讨如何使用React Hooks来优化前端应用的性能,重点介绍Hooks在状态管理、副作用处理和组件逻辑复用方面的应用。通过本文的指导,读者将了解到如何利用React Hooks提升前端应用的响应速度和用户体验。
|
4月前
|
前端开发 JavaScript
使用 MobX 优化 React 代码
使用 MobX 优化 React 代码
38 0
|
6月前
|
JavaScript 前端开发 虚拟化
理解React页面渲染原理,如何优化React性能?
理解React页面渲染原理,如何优化React性能?
66 0
|
2月前
|
前端开发
利用React Hooks优化前端状态管理
本文将深入介绍如何利用React Hooks优化前端状态管理,包括Hooks的概念、使用方法以及与传统状态管理方式的对比分析,帮助前端开发人员更好地理解和应用这一现代化的状态管理方案。
|
3月前
|
前端开发 JavaScript
React渲染性能的优化
React渲染性能的优化
27 2
|
4月前
|
前端开发 JavaScript API
React有哪些优化性能的手段?
React有哪些优化性能的手段
17 0
|
5月前
|
前端开发 JavaScript
React中通过children prop或者React.memo来优化子组件渲染【react性能优化】
React中通过children prop或者React.memo来优化子组件渲染【react性能优化】
46 0
|
5月前
|
前端开发 JavaScript UED
react中的forwardRef 和memo的区别?
react中的forwardRef 和memo的区别?
45 0
|
5月前
|
存储 前端开发 JavaScript
React Memo不是你优化的第一选择(二)
React Memo不是你优化的第一选择(二)
|
4月前
|
设计模式 前端开发 数据可视化
【第4期】一文了解React UI 组件库
【第4期】一文了解React UI 组件库
122 0