当前位置:首页 > 科技  > 软件

我已彻底拿捏 React Compiler,原来它是元素级细粒度更新。原理性能优秀实践都在这七千字里

来源: 责编: 时间:2024-06-18 17:04:28 68观看
导读说实话现在我很激动。从 React Compiler 开源到现在我连续研究分析 React Compiler 已经四天时间了,这期间我积累了大量的使用心得,整体感受就是它真的太强了!现在我迫不及待地想跟大家分享 React Compiler 的深度使用体

说实话现在我很激动。OsD28资讯网——每日最新资讯28at.com

从 React Compiler 开源到现在我连续研究分析 React Compiler 已经四天时间了,这期间我积累了大量的使用心得,整体感受就是它真的太强了!OsD28资讯网——每日最新资讯28at.com

现在我迫不及待地想跟大家分享 React Compiler 的深度使用体验。OsD28资讯网——每日最新资讯28at.com

这篇文章我会结合三个实践案例为大家解读 React Compiler 到底强在哪,这可能会有一点难理解,不过道友们请放心,我会做好知识铺垫,尽量用更简单的方式来表达。内容梗概如下:OsD28资讯网——每日最新资讯28at.com

  • 如何查看编译之后的代码
  • Symbol.for() 基础介绍
  • 实现原理详细分析
  • 实践案例一:counter 递增
  • 实践案例二:渲染成本昂贵的子组件
  • 实践案例三:Tab 切换
  • 强悍的性能表现:超细粒度缓存式/记忆化更新
  • 项目开发中,最佳实践应该怎么做

经过验证发现由于 React19 之前的版本内部不包含 compiler-runtime,因此无法正常使用,我猜测可能会在以后提供插件来支持编译老版本的项目。目前我是在 React 19 RC 版本中结合 Compiler。不过好消息是将项目升级到 React 19 难度并不高。许多三方库也已经积极的适配了 React 19。OsD28资讯网——每日最新资讯28at.com

OsD28资讯网——每日最新资讯28at.com

从演示效果上来看,这是一个普通的 tab 切换。但是先别急,我还有要求。我希望能实现极限的性能优化。OsD28资讯网——每日最新资讯28at.com

  • 我希望首次渲染时,页面渲染更少的内容,因此此时,只能先渲染默认的 Panel。其他 Panel 需要在点击对应的按钮时,才渲染出来。
  • 在切换过程中,我希望能够缓存已经渲染好的 Panel,只需要在样式上做隐藏,而不需要在后续的交互中重复渲染内容
  • 当四个页面都渲染出来之后,再做切换时,此时只会有两个页面会发生变化,上一个选中的页面与下一个选中的页面。另外的页面不参与交互,则不应该 re-render。

OsD28资讯网——每日最新资讯28at.com

这个案例和要求不算特别难,但是对综合能力的要求还是蛮高的,大家有空可以自己尝试实现一下,看看能不能完全达到要求。OsD28资讯网——每日最新资讯28at.com

具体的完整实现我们会在后续的直播中跟大家分享。大家可以加我好友「icanmeetu」然后进 React19 讨论群,React19 相关的直播消息会第一时间在群内公布。OsD28资讯网——每日最新资讯28at.com

这里,我主要想跟大家分享的就是 map 方法的小细节。有如下代码:OsD28资讯网——每日最新资讯28at.com

{tabs.map((item, index) => {  return (    <item.component      appearder={item.appeared}      key={item.title}      selected={current === index}    />  )})}

它的编译结果表现如下:OsD28资讯网——每日最新资讯28at.com

let t4;if ($[7] !== current) {  t4 = tabs.map((item_0, index_1) => (    <item_0.component      appearder={item_0.appeared}      key={item_0.title}      selected={current === index_1}    />  ));  $[7] = current;  $[8] = t4;} else {  t4 = $[8];}

我们会发现,此时编译缓存的是整个 map 表达式,但是由于 map 表达式又依赖于 current,因此,在我们点击切换的交互过程中,每一次的 current 都会发生变化,那么这里针对 map 表达式的缓存就没有了任何意义。OsD28资讯网——每日最新资讯28at.com

但是实际上,我们可以观察到,我们有 4 个 Panel,点击切换的交互发生时,实际上只有两个 Pannel 发生了变化。因此,最极限的优化是,只有这两个组件对应的函数需要重新 re-render,那么我们的代码应该怎么写呢?OsD28资讯网——每日最新资讯28at.com

其实非常简单,那就是不用 map,将数组拆开直接手写,代码如下:OsD28资讯网——每日最新资讯28at.com

let c1 = tabRef.current[0]let c2 = tabRef.current[1]let c3 = tabRef.current[2]let c4 = tabRef.current[3]
<c1.component appearder={c1.appeared} selected={current === 0}/><c2.component appearder={c2.appeared} selected={current === 1}/><c3.component appearder={c3.appeared} selected={current === 2}/><c4.component appearder={c4.appeared} selected={current === 3}/>

然后,我们就会发现,在编译结果中,不再缓存 map 表达式的结果,而是缓存每一个组件。OsD28资讯网——每日最新资讯28at.com

let t5;if ($[7] !== c1.component || $[8] !== c1.appeared || $[9] !== t4) {  t5 = <c1.component appearder={c1.appeared} selected={t4} />;  $[7] = c1.component;  $[8] = c1.appeared;  $[9] = t4;  $[10] = t5;} else {  t5 = $[10];}

OsD28资讯网——每日最新资讯28at.com

这样做的收益在特定场景下的收益将会非常高。OsD28资讯网——每日最新资讯28at.com

七、强悍的性能:细粒度记忆化更新

经过上面的学习,想必各位道友对 React Compiler 的工作机制已经有了非常深刻的理解。此时,我们就需要分析一下,这样的记忆化更新机制,到底有多强。OsD28资讯网——每日最新资讯28at.com

首先明确一点,和 Vue 等其他框架的依赖收集不同,React Compiler 依然不做依赖收集。OsD28资讯网——每日最新资讯28at.com

React 依然通过从根节点自上而下的 diff 来找出需要更新的节点。在这个过程中,我们会通过大量的判断来决定使用缓存值。可以明确的是,Compiler 编译之后的代码,缓存命中的概率非常高,几乎所有应该缓存的元素和函数都会被缓存起来。OsD28资讯网——每日最新资讯28at.com

因此,React Compiler 也能够在不做依赖收集的情况下,做到元素级别的超级细粒度更细。但是,这样做的代价就是,React 需要经历大量的判断来决定是否需要使用缓存结果。OsD28资讯网——每日最新资讯28at.com

所以这个时候,我们就需要明确,我所谓的大量判断的时间成本,到底有多少?它会不会导致新的性能问题?OsD28资讯网——每日最新资讯28at.com

可以看到,Compiler 编译之后的代码中,几乎所有的比较都是使用了全等比较,因此,我们可以写一个例子来感知一下,超大量的全等比较到底需要花费多少时间。OsD28资讯网——每日最新资讯28at.com

测试代码如下:OsD28资讯网——每日最新资讯28at.com

var cur = performance.now()for(let i = 0; i < 1000000; i++) {  'xxx' == 'xx'}var now = performance.now()console.log(now - cur)

执行结果,比较 100 万次,只需要花费不到 1.3 毫秒。这太强了啊。我们很难有项目能够达到 1000,000 次的比较级别,甚至许多达到 10000 都难。那也就意味着,这里大量的比较成本,落实到你的项目中,几乎可以忽略不计。OsD28资讯网——每日最新资讯28at.com

OsD28资讯网——每日最新资讯28at.com

为了对比具体的效果,我们可以判断一下依赖收集的时间成本。OsD28资讯网——每日最新资讯28at.com

首先是使用数组来收集依赖。依然是 100 万次收集,具体执行结果如下。耗时 8 毫秒。OsD28资讯网——每日最新资讯28at.com

OsD28资讯网——每日最新资讯28at.com

使用 Map 来收集依赖。100 万次依赖收集耗时 54 ms。OsD28资讯网——每日最新资讯28at.com

OsD28资讯网——每日最新资讯28at.com

使用 WeakMap 来收集依赖,那就更慢了。100万次依赖收集耗时 200 毫秒。OsD28资讯网——每日最新资讯28at.com

OsD28资讯网——每日最新资讯28at.com

WeakMap 的 key 不能是一个 number 类型。OsD28资讯网——每日最新资讯28at.com

OsD28资讯网——每日最新资讯28at.com

数据展示给大家了,具体强不强,大家自行判断。OsD28资讯网——每日最新资讯28at.com

OsD28资讯网——每日最新资讯28at.com

这里我要明确的是,这样的性能表现,在之前版本的项目中,合理运用 useCallback/memo 也能做到。只是由于对 React 底层默认命中规则不理解,导致大多数人不知道如何优化到这种程度。React Compiler 极大的简化了这个过程。OsD28资讯网——每日最新资讯28at.com

八、React Compiler 最佳实践

有许多骚操作,React Compiler 并不支持,例如下面这种写法。OsD28资讯网——每日最新资讯28at.com

{[1, 2, 3, 4, 5].map((counter) => {  const [number, setNumber] = useState(0)  return (    <div key={`hello${counter}`} onClick={() => setNumber(number + 1)}>      number: {number}    </div>  )})}

这个操作骚归骚,但是真的有大佬想要这样写。React 之前的版本依然不支持这种写法。不过好消息是,React 19 支持了...OsD28资讯网——每日最新资讯28at.com

但是 React Compiler 并不支持。对于这些不支持的语法,React Compiler 的做法就是直接跳过不编译,而直接沿用原组件写法。OsD28资讯网——每日最新资讯28at.com

因此,React Compiler 的最佳实践我总结了几条OsD28资讯网——每日最新资讯28at.com

  • 1、不再使用 useCallback、useMemo、Memo 等缓存函数
  • 2、丢掉闭包的心智负担,放心使用即可
  • 3、引入严格模式
  • 4、在你不熟悉的时候引入 eslint-plugin-react-compiler
  • 5、当你熟练之后,弃用它,因为有的时候我们就是不想让它编译我们的组件
  • 6、更多的使用 use 与 Action 来处理异步逻辑
  • 7、尽可能少使用 useEffect

这里,一个小小的彩蛋就是,当你不希望你的组件被 Compiler 编译时,你只需要使用 var 来声明状态即可。因为这不符合它的语法规范OsD28资讯网——每日最新资讯28at.com

var [counter, setCounter] = useState(0)

而你改成 const/let,它就会又重新编译该组件。可控性与自由度非常高。OsD28资讯网——每日最新资讯28at.com

本文链接:http://www.28at.com/showinfo-26-94585-0.html我已彻底拿捏 React Compiler,原来它是元素级细粒度更新。原理性能优秀实践都在这七千字里

声明:本网页内容旨在传播知识,若有侵权等问题请及时与本网联系,我们将在第一时间删除处理。邮件:2376512515@qq.com

上一篇: 如果没有这个 JavaScript 功能,95%的用户会讨厌使用你的应用程序

下一篇: 一个数据获取竟被 React Query 玩出这么多花样来!

标签:
  • 热门焦点
Top