大家好,我是Kason。在中文社区,流传了这么多年的说法:JS线程负责执行JS,GUI渲染线程负责渲染。两者是互斥的,所以JS执行会阻塞渲染。但是随着DevTools的使用越来越多,我渐渐开始怀疑上面的说法。本文将通过实际案例来解释为什么JS会阻塞渲染。欢迎加入人类优质前端框架群,一起来看看几个线程在讲解JS线程和GUI线程互斥的文章中,通常会列出渲染进程中包含的线程,例如:GUI渲染线程JS引擎线程事件触发线程定时触发线程、HTTP请求线程等。不过,我们还是以百度搜索页面为例,打开Performance面板,开始录制:在上图的录制结果中:Chrome_ChildIOThread对应的是IO线程的任务记录,用户输入、网络、设备相关的事件都与之相关RasterRecord光栅化线程池任务,GPU记录GPU复合位图任务,Compositor记录复合线程任务执行。以上三者都与浏览器渲染有关。Main记录了渲染进程主线程中的任务。从这个角度来看,浏览器实际的线程情况与GUI线程相关的文章中描述的并不相同。主线程的任务接下来,让我们继续Main。红线框内不同长度的灰色块是在主线程中执行的任务。注意红色框内的绿色方块FP,它代表FirstPaint(第一次绘制):那么在第一次绘制之前必须执行哪些任务呢?可以看出主要有3个Tasks(任务):第一个任务是请求HTML数据:ParseHTML当请求返回HTML字节流时,开始第二个任务,将HTML字节流解析成DOM。名字就是图中的蓝色块ParseHTML:注意有一些EvaluateScripts执行时间不同。这些是解析DOM树过程中遇到的JS代码。从DOM树中可以看到这些阻塞DOM树生成的JS脚本:它们的存在显着延长了解析HTML的时间。RecaculateStyle解析完DOM树(蓝色的ParseHTML)之后,接下来的任务就是紫色的RecaculateStyle:他负责将HTML中的CSS样式(outline,inline)输出为styleSheets,styleSheets有两个作用:可以和DOM树结合组合为页面带来风格。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的执行时间过长时,渲染相关任务来不及执行。
