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

相关文章
|
27天前
|
存储 缓存 JavaScript
如何优化React或Vue应用的性能
需要注意的是,性能优化是一个持续的过程,需要根据具体的应用场景和性能问题进行针对性的优化。同时,不同的项目和团队可能有不同的优化重点和方法,要结合实际情况灵活运用这些优化策略,以达到最佳的性能效果。
107 51
|
11天前
|
前端开发 UED 开发者
React 选项卡组件 Tabs:从基础到优化
本文详细介绍了如何在React中构建一个功能丰富的选项卡组件,包括基础实现、样式美化、常见问题及解决方法。通过逐步讲解,从简单的选项卡组件结构开始,逐步引入样式、性能优化、动态内容加载、键盘导航支持和动画效果,最后讨论了自定义样式的实现。旨在帮助开发者在React项目中高效构建高质量的选项卡组件。
47 18
|
16天前
|
前端开发 UED
React 文本区域组件 Textarea:深入解析与优化
本文介绍了 React 中 Textarea 组件的基础用法、常见问题及优化方法,包括状态绑定、初始值设置、样式自定义、性能优化和跨浏览器兼容性处理,并提供了代码案例。
42 8
|
1月前
|
前端开发 JavaScript API
探究 React Hooks:如何利用全新 API 优化组件逻辑复用与状态管理
本文深入探讨React Hooks的使用方法,通过全新API优化组件逻辑复用和状态管理,提升开发效率和代码可维护性。
|
2月前
|
前端开发
React Memo
10月更文挑战第11天
41 6
|
2月前
|
前端开发 JavaScript 算法
React 渲染优化策略
【10月更文挑战第6天】React 是一个高效的 JavaScript 库,用于构建用户界面。本文从基础概念出发,深入探讨了 React 渲染优化的常见问题及解决方法,包括不必要的渲染、大量子组件的渲染、高频事件处理和大量列表渲染等问题,并提供了代码示例,帮助开发者提升应用性能。
59 6
|
2月前
|
JSON 前端开发 JavaScript
【简单粗暴】如何使用 React 优化 AG 网格性能
【简单粗暴】如何使用 React 优化 AG 网格性能
35 3
|
3月前
|
缓存 前端开发
React中函数式Hooks之memo、useCallback的使用以及useMemo、useCallback的区别
React中的`memo`是高阶组件,类似于类组件的`PureComponent`,用于避免不必要的渲染。`useCallback` Hook 用于缓存函数,避免在每次渲染时都创建新的函数实例。`memo`可以接收一个比较函数作为第二个参数,以确定是否需要重新渲染组件。`useMemo`用于缓存计算结果,避免重复计算。两者都可以用来优化性能,但适用场景不同:`memo`用于组件,`useMemo`和`useCallback`用于值和函数的缓存。
105 1
|
7月前
|
监控 前端开发 API
如何优化React性能?
【4月更文挑战第9天】提升React应用性能的关键策略包括:使用React.memo和PureComponent防止不必要的重渲染;实施代码分割减少初始加载时间;借助React Profiler定位性能问题;优化state和props以减小大小和复杂性;设置列表项的key属性;避免内联函数和对象;自定义shouldComponentUpdate或React.memo比较函数;使用虚拟化技术渲染大量列表;通过Context API共享数据;以及利用服务端渲染加速首屏加载。不断监控和调整是优化的核心。
66 9
|
4月前
|
前端开发 JavaScript 大数据
React与Web Workers:开启前端多线程时代的钥匙——深入探索计算密集型任务的优化策略与最佳实践
【8月更文挑战第31天】随着Web应用复杂性的提升,单线程JavaScript已难以胜任高计算量任务。Web Workers通过多线程编程解决了这一问题,使耗时任务独立运行而不阻塞主线程。结合React的组件化与虚拟DOM优势,可将大数据处理等任务交由Web Workers完成,确保UI流畅。最佳实践包括定义清晰接口、加强错误处理及合理评估任务特性。这一结合不仅提升了用户体验,更为前端开发带来多线程时代的全新可能。
103 1