当前位置: 首页 > 科技观察

JS阻塞渲染,我误会了这么多年?

时间:2023-03-22 01:15:46 科技观察

大家好,我是Kason。在中文社区,流传了这么多年的说法:JS线程负责执行JS,GUI渲染线程负责渲染。两者是互斥的,所以JS执行会阻塞渲染。但是随着DevTools的使用越来越多,我渐渐开始怀疑上面的说法。本文将通过实际案例来解释为什么JS会阻塞渲染。到底有多少个线程在讲解JS线程和GUI线程互斥的文章中,通常会列出渲染进程中包含的线程,如:GUI渲染线程。JS引擎线程。事件触发线程。定时触发线程。HTTP请求线程。不过我们还是以百度的搜索页面为例,打开Performance面板,开始记录:在上图的记录结果中:Chrome_ChildIOThread对应IO线程的任务记录,以及用户输入、网络、设备相关的事件都与它有关。Raster记录光栅化线程池任务,GPU记录GPU合成位图任务,Compositor记录合成线程任务执行,这些都与浏览器渲染有关。Main记录渲染进程主线程中的任务。从这个角度来看,浏览器的实际线程情况与GUI线程相关的文章中描述的并不相同。主线程的任务接下来,让我们继续Main。红线框内不同长度的灰色块是在主线程中执行的任务。注意红框内的绿色方块FP,它代表FirstPaint(第一次绘制):那么第一次绘制之前应该执行哪些任务呢?可以看到主要有3个Tasks(任务):第一个任务是请求HTML数据:ParseHTML请求回HTML字节流后,开始第二个任务,将HTML字节流解析成DOM,这个任务的名字就是图中的蓝色块ParseHTML:注意其中有一些执行时间不同的EvaluateScript,这些都是在解析DOM树的过程中遇到的JS代码。从DOM树中可以看到这些阻塞DOM树生成的JS脚本:它们的存在显着延长了解析HTML的时间。RecaculateStyle解析完DOM树后(蓝色的ParseHTML),接下来的任务是紫色的RecaculateStyle:他负责将HTML中的CSS样式(outline,inline)输出为styleSheets,styleSheets有两个作用:可以和DOMTree绑定为页面带来风格。JS可以操作styleSheets改变页面样式。我们可以从控制台打印document.styleSheets来直观感受它的存在:Layout有一个DOM树和styleSheets,然后需要为视图可见的部分生成树(比如display:none部分不需要)显示在这棵树中)。这个任务是紫色的Layout:UpdateLayerTree用户看到的页面其实是多层页面重叠的结果。开发人员可以使用多种方法(例如z-index)来更改某个部分的级别。比如滚动条会形成自己独立的层级:既然是多层结构,就需要更新每一层的信息。这个任务就是紫色的UpdateLayerTree:Paint。我们可以发现,在FP之前,只有在UpdateLayerTree之后,我们才去完成Paint的任务:从字面意思来说,这就是“画图”吗?不会。Paint的任务是将每一层页面的绘制信息组织起来形成一个绘制列表,这些数据会交给合成线程进行后续的绘制操作。可以发现,具体的绘制操作是由合成线程完成的,与JS所在线程(主线程)并不互斥。为什么JS会阻塞渲染?我们现在知道JS执行和Paint任务都发生在主线程上。“渲染被阻塞”的原因很明显:因为Paint任务没有及时执行,也就是绘图列表没有及时提交给合成线程。之所以没有及时执行,可能是JS执行时间太长,导致没有来得及执行这一帧的Paint。比如我们打开B站,记录主线程的任务。可以看到一个JS的执行时间达到了231.88ms,超过了一帧的时间。这段时间主线程没有时间执行Paint:综上所述,JS之所以阻塞渲染,是因为JS执行和“渲染相关任务”都在争夺主线程的有限资源。当JS执行时间过长时,“渲染相关任务”来不及执行。