前言
react 18 alpha为了在用户体验上面取得进展,做了不少的改动。
- startTransition 当状态变化需要耗费大量时间的时候,保证UI有更好的响应
- useDeferredValue 让屏幕上不重要的一部分可以延迟更新
可以让开发者控制加载进度条的显示顺序
下面是react核心员工ricky工作一年的过程和回顾。希望通过他的描述,我们对react 新增的一些特性有更全面的了解。同时也可以对如何迭代一个大型开源项目拥有个更近的视角。
ricky的一年回顾
2021年6月10日,react核心开发者ricky回顾了在发布react 18 阿尔法版之前一年内的心路历程。希望通过这些描述让广大开发者近距离的了解react核心团队是如何给react添加新特性的。
当我加入react团队的时候,他们已经深入研究了并发特性(concurrent)好多年。我花费了大量时间阅读和提问,来搞明白我们已经知道了什么以及我们未来如何规划。
由于我缺乏上下文,我开始主要是遵守团队的意见来开展工作。但是成为高级别的工程师的之后,我觉得很难再像之前那样了。我觉得是我自己已经可以自主来推进react项目了。
幸运的是,我和acdlitte一块共事。他已经思考并发这个问题好多年了,并且已经找到了很好的解决方案。过去一年中,我们开始进行了大量30分钟的一对一沟通,最终演变成了两三个小时的深度讨论。这样的师徒关系让我感到太幸运了。
团队已经取得了很多的进展。2019年他们已经开始了测试,但是在这个模型里面他们发现了一些缺陷,然后又进行了完全的重新设计实现。2020年,acdlitte完成了lanes的实现,所以我加入的时候,团队正着手并发api的设计和实现。
我接到的第一个任务是关于startTransition的,任务的目的是修复react和scheduler之间的分层问题。那时对startTransition的定义和现在不太一样。
当我刚开始研究并发渲染的时候,ProvablyFlarnie创建了一个叫做调度器(Scheduler)的包,来在浏览器工作和react渲染之间进行调度。调度器的设计思路是“用户代码给调度器设置优先级,react利用优先级信息来进行更新”。
我们考虑使用像startTransition和flushSync这样的React api,而不是直接调用调度程序。这样重写会很麻烦,并且有很多未知的东西,但它允许我们在React 18中引入更简单的api。
事实上,在重写泳道(lanes)之前,不可能做出——甚至不可能想到——这种改变(这就是为什么从一开始就不是这样的)。但正如研究中经常出现的,在一个领域看法的改变可能会引发对其他领域的重新思考。
因为我们已经在产品中测试了现有的api,所以我们需要在不回归任何性能指标的情况下就地重写它。这意味着我们需要让两个版本同时工作,并比较我们在A/B测试中所做的每一个改变。
我们非常幸运能够在新的Facebook网站上进行研究,因为它的工具和规模意味着我们可以检测到性能上的非常小的变化,这可能意味着我们的模型或实现中的缺陷。
我们也很幸运能和b56girard、alannorbauer这样的人一起工作
他们帮助我们进行了正确的实验。并且当度量指标的时候,他们愿意帮助我们找到bug。
这个实验迭代对于React的成功是至关重要的,因为这个阶段虽然可能会花费我们几个月的时间来修复找到的bug。但如果我们将这些bug推广到整个社区,几年也可能解决不完。
除了重写React内部代码外,我们还需要对所有调用Scheduler api(如schedul.runwithpriority)的代码进行修改,并将它们更改为调用React.startTransition、react.flushSync这样的api。
为了做到这一点,我手动检查了数百个使用旧api站点的用例,确认它在语义上是相同的,更新它,记录我们没有考虑的用例,并与团队讨论这些用例。
我们和使用api的工程师进行了大量讨论。我特别感rhagigi,他负责许多核心基础设施的工作。并且当用例不清楚的时候向我解释用例,他强烈主张保持对用例的支持。
此间,我也和sebmarkbage谈了很多react设计相关的问题。我告诉他为什么某个用例没有覆盖到,但是他告诉我:“我的思考问题的方式是错的,应该按照正确(他的方式)的方式思考”。
在react开发团队,我们的迭代方式是:提出个想法,大规模发布一些api,看看运行的情况,检查测试用例,验证假设,迭代api。如果,中间发现问题,我们会重新开始新的一轮迭代。
在此期间,lunarruan和brian_d_vaughn在研究如何重构核心算法来触发副作用(effects)。这个重构很复杂,但是对Suspense和StrictMode这样的新特性很关键。
当上面说的工作完成后,新特性的开发工作基本完成了。现在我们考虑采用什么样的策略,来让 react的Concurrent模块可以无缝升级。
对于如何平滑的升级到Concurrent React,我们有一些初步的想法。但是,我们需要进行大量的代码修改,大量的观测来验证我们的假设,并且确保新的改动不会降低性能,并支持所有的用例。
我们做了很多修改,例如利用并发新特性来选择时间片,确保react事件系统内部的事件和外部的事件同样的进行刷新。
在我重构调度器做的基础上,dan_abramov实现了原生事件刷新。他做的非常好,原生事件刷新速度快了5倍,代码质量也高了5倍。(5+5=10倍开发人员)。
有一次,我们在20天内进行了6次实验,没有出现任何重大回归问题(这很令人惊讶,因为它包含了我的代码)。这极大地加速我们的进度。中间获得的经验也让我们决定选择性加入时间片(time-slicing)。
到四月的时候,新api已经足够稳定了,我们已经做好了发布react 18 alpha的准备。但是因为Suspense和batching有一些语义上面的变化,我们需要确保可以无痛升级才能进行发布。
为了验证我们的新策略,我们使用新策略升级了一些内部和外部的大型app。这些app大范围覆盖了我们的用例,并且都没遇到什么大问题,甚至自动的批处理操作(automated batching)让他们中的一些有了性能的提升。
这让我们相信升级策略是可靠的,我们准备发布新的React 18alpha。
React 核心团队的工作是非常困难的,有时候进展也很缓慢。但是我们必须要确保我们提供的是社区需要的功能,让所有的用户有更好的体验,带来最小的破坏性变化,并且长期来看不存在太多的麻烦和妥协。
感谢阅读!今年是疯狂的一年,我希望这篇文章能让你们了解到在React核心团队工作是什么感觉✨