JavaScript和CSS如何阻塞HTML解析和渲染

950 阅读3分钟

开头的废话

关于JavaScript和CSS导致的阻塞问题,其实很容易在网上找到答案:

  • 加载和执行JavaScript脚本会阻塞HTML的解析(不讨论deferasync脚本)
  • 加载和解析外部样式不会阻塞HTML的解析,但阻塞页面的渲染

但最近写的一段Demo却发现CSS阻塞了HTML的解析,相关的文章并没有提到这个场景,因而决定自己梳理一遍。

1、预解析器和主解析器

通常浏览器在解析HTML时,不仅仅只有一个解析器在工作,而是预解析器和主解析器可能同时工作。其中,主解析器负责解析HTML生成DOM树;预解析器不会修改DOM树,它主要负责扫描整个HTML文档,寻找需要网络加载的外部资源(如:脚本、样式表和图片等),将这些资源交给浏览器的网络层去加载。预解析的机制是浏览器的一种优化手段,Firefox和Webkit都支持这一项优化(相关内容可参考《浏览器的工作原理:新式网络浏览器幕后揭秘》)。

以下面的代码为例:

<html>
  <body>
    <script>
        while (true) { }
    </script>
    <script src="index.js"></script>
  </body>
</html>

其中,第一个脚本包含死循环,但第二个脚本依然可以正常的下载,不会因为第一个脚本的死循环而阻塞,这就是预加载器的功劳。 speculative-parser.png

2、JavaScript

以下面的代码为例,页面首先渲染Before Script!,经过一段时间的加载之后,控制台会输出Hello, Index!,最后页面渲染After Script!。因此,看起来的效果是JavaScript的加载和执行阻塞了HTML的解析和页面的渲染。

<html>
  <body>
    <div>Before Script! </div>
    <script src="index.js"></script>
    <div>After Script! </div>
  </body>
</html><!-- index.js -->
console.log('Hello, Index!')

我们打开页面加载的火焰图查看其中的细节。很明显,浏览器接收到HTML之后,还没有开始解析,对<script>的网络请求就已经发送,这就是预解析器在工作。发送请求之后,才开始解析HTML。按照这个逻辑来看,加载脚本的过程似乎不会阻塞HTML的解析,但先不急于下结论,继续往下看。 javascript-block-analysis1.png 本次Parse HTML结束之后,接下来就直接执行布局,完成首屏的绘制,此时屏幕上仅仅绘制出Before Script!,显然是加载脚本导致<script>之后的<div>未被渲染出来。 javascript-block-analysis2.png 脚本加载结束之后,浏览器开始执行脚本,接着又继续解析HTML、重新布局和绘制。至此,整个页面的加载过程才结束。既然脚本执行之后又再次解析HTML,说明第一次解析HTML没有解析完成,换而言之,加载和执行脚本会阻塞HTML的解析。 javascript-block-analysis3.png 到这里得出结论:JavaScript脚本的加载和执行,会阻塞解析该脚本之后的HTML,进而阻塞DOM树的生成和页面渲染。

3、CSS

通常我们会在<head>内使用<link>标签加载外部样式,样式的加载和解析不会阻塞解析HTML,但阻塞页面的渲染。这个结论可以在很多文章中看到,依据火焰图也很容易验证这个结论,因此不做这个场景详细的分析。

在这里验证另一种情况,就是<link>标签并没有放在<head>中,而是放在<body>内。以如下代码为例:

<html>
  <body>
    <div>Before CSS! </div>
    <link rel="stylesheet" href="index.css">
    <div>After CSS! </div>
  </body>
</html>

这种情况和JavaScript脚本的加载过程很类似:预解析器找到需要加载的外部资源,发起请求;主解析器执行HTML解析,再执行布局和绘制。实际绘制的结果也只有Before CSS!,这样看来,加载CSS的确阻塞了页面的渲染,但它是否阻塞了HTML的解析,则需要观察CSS加载和解析完成之后,是否再次执行了HTML解析。 css-block-analysis1.png 当CSS加载完毕之后,先解析样式表,之后再次解析了HTML、重新布局和绘制。这就可以说明,HTML没有在最开始解析完成,加载和解析CSS阻塞了HTML的解析。 css-block-analysis2.png 总结:<body>内使用<link>加载外部样式,不仅仅阻塞页面的渲染,同样也会阻塞HTML的解析

4、其他场景

先来看一段代码:

<html>
  <head>
    <link rel="stylesheet" href="index.css">
    <script>
        console.log(document.querySelectorAll('div'))
    </script>
  </head>
  <body>
    <div>Hello, Index! </div>
  </body>
</html>

根据上面总结的内容,预解析器会提前调用网络层加载外部样式,加载和解析<head>中的样式不会阻塞HTML解析,但脚本会阻塞HTML解析。因此,我们做一些猜测:主解析器一直解析到<script>脚本并执行,再继续完成解析,等待样式加载和解析结束,两者结合之后就会开始布局和渲染。

实际情况我们可以查看火焰图,最开始的确是提前加载index.css,再开始解析HTML。 mixed-block-analysis1.png 但后面的流程和之前的猜想不完全一致,请求拿到index.css之后,先执行了样式的解析,再执行JavaScript脚本,最后才解析剩余的HTML、重新布局渲染。也就是说,JavaScript脚本被阻塞到样式解析完成才执行。 mixed-block-analysis2.png 这个效果其实验证了另一个结论:浏览器在样式表的加载和解析过程会禁止部分JavaScript脚本,该结论也可参考《浏览器的工作原理:新式网络浏览器幕后揭秘》。

5、总结

这篇文章主要验证了以下几点结论:

  • JavaScript脚本的加载和执行会阻塞HTML的解析和页面渲染
  • 外部的CSS样式放在<head>内,加载过程不会阻塞HTML的解析,但是会阻塞页面的渲染
  • 外部的CSS样式放在<body>内,加载过程会阻塞HTML的解析,也会阻塞页面的渲染
  • 浏览器在样式表的加载和解析过程会禁止部分JavaScript脚本

而验证这样的结论出于两个目的:

  • 更有利于理解浏览器引擎的解析和渲染机制。
  • 了解常见优化方法的原因,例如:CSS要在<head>内引入;JavaScript脚本最好在页面末尾引入。