#性能优化-白屏度量 阿里是怎么监控前端白屏的?,2024年五面蚂蚁

render

现在我们要开始渲染页面,是我们刚才的例子,执行 ReactDOM.render 。这里我们有个全局 workInProgress 对象标志当前处理的 FiberNode

  1. 首先我们为根节点初始化一个 FiberNodeRoot ,他的结构就如上面所示,并将 workInProgress= FiberNodeRoot

  2. 接下来我们执行 ReactDOM.render 方法的第一个参数,我们得到一个 ReactElement :

ReactElement = {

$$typeof: Symbol(react.element),

key: null,

props: {

children: {

$$typeof: Symbol(react.element),

key: null,

props: {},

ref: null,

type: ƒ Child(),

}

}

ref: null,

type: f App()

}

该结构描述了 <App><Child /></App>

  1. 我们为 ReactElement 生成一个 FiberNode 并把 return 指向父 FiberNode ,最开始是我们的根节点,并将 workInProgress = FiberNode

{

elementType: f App(), // type 就是 App 函数

key: null,

type: FunctionComponent, // 函数组件类型

return: FiberNodeRoot, // 我们的根节点

child: null,

sibling: null,

flags: null

}

  1. 只要workInProgress 存在我们就要处理其指向的 FiberNode 。节点类型有很多,处理方法也不太一样,不过整体流程是相同的,我们以当前函数式组件为例子,直接执行 App(props) 方法,这里有两种情况

  2. 该组件 return 一个单一节点,也就是返回一个 ReactElement 对象,重复 3 - 4 的步骤。并将当前 节点的 child 指向子节点 CurrentFiberNode.child = ChildFiberNode 并将子节点的 return 指向当前节点 ChildFiberNode.return = CurrentFiberNode

  3. 该组件 return 多个节点(数组或者 Fragment ),此时我们会得到一个 ChildiFberNode 的数组。我们循环他,每一个节点执行 3 - 4 步骤。将当前节点的 child 指向第一个子节点 CurrentFiberNode.child = ChildFiberNodeList[0] ,同时每个子节点的 sibling 指向其下一个子节点(如果有) ChildFiberNode[i].sibling = ChildFiberNode[i + 1] ,每个子节点的 return 都指向当前节点 ChildFiberNode[i].return = CurrentFiberNode

如果无异常每个节点都会被标记为待布局 FiberNode.flags = Placement

  1. 重复步骤直到处理完全部节点 workInProgress 为空。

最终我们能大概得到这样一个 FiberNode 树:

FiberNodeRoot = {

elementType: null,

type: HostRoot,

return: null,

child: FiberNode,

sibling: null,

flags: Placement, // 待布局状态

}

FiberNode {

elementType: f App(),

type: FunctionComponent,

return: FiberNodeRoot,

child: FiberNode

,

sibling: null,

flags: Placement // 待布局状态

}

FiberNode

 {

elementType: ‘p’,

type: HostComponent,

return: FiberNode,

sibling: FiberNode,

child: null,

flags: Placement // 待布局状态

}

FiberNode {

elementType: f Child(),

type: FunctionComponent,

return: FiberNode,

child: null,

flags: Placement // 待布局状态

}

提交阶段

提交阶段简单来讲就是拿着这棵树进行深度优先遍历 child => sibling,放置 DOM 节点并调用生命周期。

那么整个正常的渲染流程简单来讲就是这样。接下来看看异常处理

错误边界流程

刚刚我们了解了正常的流程现在我们制造一些错误并捕获他:

const App = ({ children }) => (

<>

hello

{ children }

</>

);

const Child = () => 

I’m child {a.a}

const a = ReactDOM.render(

,

document.getElementById(‘root’)

);

执行步骤 4 的函数体是包裹在 try...catch 内的如果捕获到了异常则会走异常的流程:

do {

try {

workLoopSync(); // 上述 步骤 4

break;

} catch (thrownValue) {

handleError(root, thrownValue);

}

} while (true);

执行步骤 4 时我们调用 Child 方法由于我们加了个不存在的表达式 {a.a} 此时会抛出异常进入我们的 handleError 流程此时我们处理的目标是 FiberNode<Child> ,我们来看看 handleError :

function handleError(root, thrownValue): void {

let erroredWork = workInProgress; // 当前处理的 FiberNode 也就是异常的 节点

throwException(

root, // 我们的根 FiberNode

erroredWork.return, // 父节点

erroredWork,

thrownValue, // 异常内容

);

completeUnitOfWork(erroredWork);

}

function throwException(

root: FiberRoot,

returnFiber: Fiber,

sourceFiber: Fiber,

value: mixed,

) {

// The source fiber did not complete.

sourceFiber.flags |= Incomplete;

let workInProgress = returnFiber;

do {

switch (workInProgress.tag) {

case HostRoot: {

workInProgress.flags |= ShouldCapture;

return;

}

case ClassComponent:

// Capture and retry

const ctor = workInProgress.type;

const instance = workInProgress.stateNode;

if (

(workInProgress.flags & DidCapture) === NoFlags &&

(typeof ctor.getDerivedStateFromError === ‘function’ ||

(instance !== null &&

typeof instance.componentDidCatch === ‘function’ &&

!isAlreadyFailedLegacyErrorBoundary(instance)))

) {

workInProgress.flags |= ShouldCapture;

return;

}

break;

default:

break;

}

workInProgress = workInProgress.return;

} while (workInProgress !== null);

}

代码过长截取一部分 先看 throwException 方法,核心两件事:

  1. 将当前也就是出问题的节点状态标志为未完成 FiberNode.flags = Incomplete

  2. 从父节点开始冒泡,向上寻找有能力处理异常( ClassComponent )且的确处理了异常的(声明了 getDerivedStateFromError 或 componentDidCatch 生命周期)节点,如果有,则将那个节点标志为待捕获 workInProgress.flags |= ShouldCapture ,如果没有则是根节点。

completeUnitOfWork 方法也类似,从父节点开始冒泡,找到 ShouldCapture 标记的节点,如果有就标记为已捕获 DidCapture ,如果没找到,则一路把所有的节点都标记为 Incomplete 直到根节点,并把 workInProgress 指向当前捕获的节点。

之后从当前捕获的节点(也有可能没捕获是根节点)开始重新走流程,由于其状态 react 只会渲染其降级 UI,如果有 sibling 节点则会继续走下面的流程。我们看看上述例子最终得到的 FiberNode 树:

FiberNodeRoot = {

elementType: null,

type: HostRoot,

return: null,

child: FiberNode,

sibling: null,

flags: Placement, // 待布局状态

}

FiberNode {

elementType: f App(),

type: FunctionComponent,

return: FiberNodeRoot,

child: FiberNode

,

sibling: null,

flags: Placement // 待布局状态

}

FiberNode

 {

elementType: ‘p’,

type: HostComponent,

return: FiberNode,

sibling: FiberNode,

child: null,

flags: Placement // 待布局状态

}

FiberNode {

elementType: f ErrorBoundary(),

type: ClassComponent,

return: FiberNode,

child: null,

flags: DidCapture // 已捕获状态

}

FiberNode

 {

elementType: f ErrorBoundary(),

type: ClassComponent,

return: FiberNode,

child: null,

flags: Placement // 待布局状态

}

如果没有配置错误边界那么根节点下就没有任何节点,自然无法渲染出任何内容。

ok,相信到这里大家应该清楚错误边界的处理流程了,也应该能理解为什么我之前说由 ErrorBoundry 推导白屏是 100% 正确的。当然这个 100% 指的是由 ErrorBoundry 捕捉的异常基本上会导致白屏,并不是指它能捕获全部的白屏异常。以下场景也是他无法捕获的:

  • 事件处理

  • 异步代码

  • SSR

  • 自身抛出来的错误

React SSR 设计使用流式传输,这意味着服务端在发送已经处理好的元素的同时,剩下的仍然在生成 HTML,也就是其父元素无法捕获子组件的错误并隐藏错误的组件。这种情况似乎只能将所有的 render 函数包裹 try...catch ,当然我们可以借助 babel 或 TypeScript 来帮我们简单实现这一过程,其最终得到的效果是和 ErrorBoundry 类似的。

而事件和异步则很巧,虽说 ErrorBoundry 无法捕获他们之中的异常,不过其产生的异常也恰好不会造成白屏(如果是错误的设置状态,间接导致了白屏,刚好还是会被捕获到)。这就在白屏监控的职责边界之外了,需要别的精细化监控能力来处理它。

总结

那么最后总结下本文的出的几个结论:我对白屏的定义:异常导致的渲染失败。对应方案是:资源监听 + 渲染流程监听

在目前 SPA 框架下白屏的监控需要针对场景做精细化的处理,这里以 React 为例子,通过监听渲染过程异常能够很好的获得白屏的信息,同时能增强开发者对异常处理的重视。而其他框架也会有相应的方法来处理这一现象。

当然这个方案也有弱点,由于是从本质推导现象其实无法 cover 所有的白屏的场景,比如我要搭配资源的监听来处理资源异常导致的白屏。当然没有一个方案是完美的,我这里也是提供一个思路,欢迎大家一起讨论。

作者:ES2049 / 金城武

https://zhuanlan.zhihu.com/p/383686310

参考资料

[1]

文档: https://link.zhihu.com/?target=https%3A//developer.mozilla.org/zh-CN/docs/Web/API/MutationObserver

[2]

一段话: https://link.zhihu.com/?target=https%3A//zh-hans.reactjs.org/docs/error-boundaries.html%23new-behavior-for-uncaught-errors

The End

欢迎自荐投稿到《前端技术江湖》,如果你觉得这篇内容对你挺有启发,记得点个 **「在看」**哦

点个『在看』支持下 

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数前端工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Web前端开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
img
img
img
img
img
img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上前端开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录大纲截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且后续会持续更新

如果你觉得这些内容对你有帮助,可以添加V获取:vip1024c (备注前端)
img

TCP协议

  • TCP 和 UDP 的区别?
  • TCP 三次握手的过程?
  • 为什么是三次而不是两次、四次?
  • 三次握手过程中可以携带数据么?
  • 说说 TCP 四次挥手的过程
  • 为什么是四次挥手而不是三次?
  • 半连接队列和 SYN Flood 攻击的关系
  • 如何应对 SYN Flood 攻击?
  • 介绍一下 TCP 报文头部的字段
  • TCP 快速打开的原理(TFO)
  • 说说TCP报文中时间戳的作用?
  • TCP 的超时重传时间是如何计算的?
  • TCP 的流量控制
  • TCP 的拥塞控制
  • 说说 Nagle 算法和延迟确认?
  • 如何理解 TCP 的 keep-alive?

CodeChina开源项目:【大厂前端面试题解析+核心总结学习笔记+真实项目实战+最新讲解视频】

浏览器篇
  • 浏览器缓存?
  • 说一说浏览器的本地存储?各自优劣如何?
  • 说一说从输入URL到页面呈现发生了什么?
  • 谈谈你对重绘和回流的理解
  • XSS攻击
  • CSRF攻击
  • HTTPS为什么让数据传输更安全?
  • 实现事件的防抖和节流?
  • 实现图片懒加载?

一个人可以走的很快,但一群人才能走的更远。如果你从事以下工作或对以下感兴趣,欢迎戳这里加入程序员的圈子,让我们一起学习成长!

AI人工智能、Android移动开发、AIGC大模型、C C#、Go语言、Java、Linux运维、云计算、MySQL、PMP、网络安全、Python爬虫、UE5、UI设计、Unity3D、Web前端开发、产品经理、车载开发、大数据、鸿蒙、计算机网络、嵌入式物联网、软件测试、数据结构与算法、音视频开发、Flutter、IOS开发、PHP开发、.NET、安卓逆向、云计算

keep-alive?

[外链图片转存中…(img-Nt1D30Sf-1712123788191)]

CodeChina开源项目:【大厂前端面试题解析+核心总结学习笔记+真实项目实战+最新讲解视频】

浏览器篇
  • 浏览器缓存?
  • 说一说浏览器的本地存储?各自优劣如何?
  • 说一说从输入URL到页面呈现发生了什么?
  • 谈谈你对重绘和回流的理解
  • XSS攻击
  • CSRF攻击
  • HTTPS为什么让数据传输更安全?
  • 实现事件的防抖和节流?
  • 实现图片懒加载?

[外链图片转存中…(img-DgNHTD1X-1712123788191)]

一个人可以走的很快,但一群人才能走的更远。如果你从事以下工作或对以下感兴趣,欢迎戳这里加入程序员的圈子,让我们一起学习成长!

AI人工智能、Android移动开发、AIGC大模型、C C#、Go语言、Java、Linux运维、云计算、MySQL、PMP、网络安全、Python爬虫、UE5、UI设计、Unity3D、Web前端开发、产品经理、车载开发、大数据、鸿蒙、计算机网络、嵌入式物联网、软件测试、数据结构与算法、音视频开发、Flutter、IOS开发、PHP开发、.NET、安卓逆向、云计算

  • 8
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值