这篇文章是第二篇,第一篇在这里。
第一篇大致讲到了浏览器从获取原始数据开始,直到把内容画到屏幕上,但是还没有完。
之后,还有第三篇(整理)在这里
渲染阻塞
当你听到“渲染阻塞”的时候,你会想到啥?我猜是,‘某些行为阻止了浏览器把内容画到屏幕上’。
的确是你猜的那样。
所以这里有了我们的第一个优化点,把最重要的HTML内容和CSS样式提供给客户端,越快越好。
DOM和CSSOM都必须在屏幕绘制之前构造完成,所以HTML和CSS都是渲染阻塞资源。
所以关键点就是你应该让客户端竟可能快地获取你的html和css,这样就能够优化首屏渲染的时间。
JavaScript登场
现在,基本只要是个网站都有JavaScript。。。
JavaScript是会修改页面内容以及样式的。作为实现,你能用JS从DOM树里添加删除元素,还能够修改元素的CSSOM的属性。
这很好,但是这也需要付出代价。
考虑一下下面的HTML文档:
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Medium Article Demo</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<p id="header">How Browser Rendering Works</p>
<div><img src="https://i.imgur.com/jDq3k3r.jpg"></div>
</body>
</html>
这是个很简单的例子,style.css内容也很简单,如下:
body {
background: #8cacea;
}
最终显示结果如下:

简单文本和图片渲染到了屏幕上。
从之前的步骤来看,经过 原始数据->字符->标记 转换后,浏览器一旦读到<link rel="stylesheet" href="style.css">这一行的时候,就回去请求这个css文件style.css,DOM的构建仍然继续,并且一旦css文件返回内容之后,这个CSSOM的构建也开始了。
当JavaScript来了之后,这个过程会发生什么样的变化呢?
记住一点,只要浏览器读到了script标签,那DOM的构建就会暂停!
整棵DOM树的创建过程会暂停,直到这个script运行完成。(这里不是指那些ready之后的脚本哈。)

因为js会去修改DOM和CSSOM,而浏览器又不能够确定这个js会做些什么,所以需要通过暂停整个DOM的创建来预防。
这样会有多糟?
让我们用上面那个例子,然后加一点基础的script进去:
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Medium Article Demo</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<p id="header">How Browser Rendering Works</p>
<div><img src="https://i.imgur.com/jDq3k3r.jpg"></div>
<script>
let header = document.getElementById("header");
console.log("header is: ", header);
</script>
</body>
</html>
在这个script里,我用id去Dom里得到一个节点header,然后打印到了console。

你有注意到这个script是在body的底部么,让我们看下如果把它放在头部会怎样:
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Medium Article Demo</title>
<link rel="stylesheet" href="style.css">
<script>
let header = document.getElementById("header");
console.log("header is: ", header);
</script>
</head>
<body>
<p id="header">How Browser Rendering Works</p>
<div><img src="https://i.imgur.com/jDq3k3r.jpg"></div>
</body>
</html>
运行后,得到的结果header是null

为什么?
其实很简单。当HTML解析器正在创建DOM树的时候,发现有一个script标签,而此时,body标签以及它的内容还没有解析出来。DOM构建被暂停直到这个script执行完。
这里又带出了另一个重要的点。
你脚本的位置很重要。

如果你以文件形式加载一个js,一样也会暂停DOM的创建,类似:<script src="app.js"></script>
如果这个app.js放在远程服务器上,并且网络很慢,需要加载3秒呢?那么,DOM的构建就要等到3秒之后再继续!!!
这是个很大的性能问题,但还不是全部。
记得JS还能够改变CSSOM,比如:
document.getElementsByTagName("body")[0].style.backgroundColor = "red";
那么,在CSSOM创建好之前,解析器读到script会怎样呢?
结果是js的执行将会暂停,直到CSSOM创建好。

所以当遇到script的时候,DOM的构建会停止,但是CSSOM并不会(【译注】不但不会,还会暂停js的执行)。
【译注】这里我想要补充一下,通过上面所说的,我们可以推出一个观点:
JS 文件不只是阻塞 DOM 的构建,它会导致 CSSOM 也阻塞 DOM 的构建。
原本 DOM 和 CSSOM 的构建是互不影响,井水不犯河水,但是一旦引入了 JavaScript,CSSOM 也开始阻塞 DOM 的构建。因为不完整的 CSSOM 是无法使用的,如果 JavaScript 想访问 CSSOM 并更改它,那么在执行 JavaScript 时,必须要能拿到完整的 CSSOM。所以就导致了一个现象,如果浏览器尚未完成 CSSOM 的下载和构建,而我们却想在此时运行脚本,那么浏览器将延迟脚本执行和 DOM 构建,直到完成 CSSOM 的创建。也就是说,在这种情况下,浏览器会先去构建 CSSOM,然后再执行 JavaScript,最后在继续构建 DOM。
async属性
默认情况下,每个script都会组织DOM树的创建。不够有一个方法能够改变这一个行为。
如果你在script标签里加一个async属性,那么DOM树的创建就不会暂停,这个脚本就会在下载完后去执行。例如:
<script src="https://some-link-to-app.js" async></script>
关键渲染路径
我们之前所说的从获取html,css和js原始数据开始一直到画到屏幕上。这整个过程就被叫做关键渲染路径(critical rendering path)。
优化网页性能就是优化这条关键渲染路径。
经过良好优化的网站应该是渐进式加载的,而不会被整个阻止。
这就是一个网站快与慢的差异所在。 一个比较好的策略是让浏览器优先加载哪些资源,加载资源的顺序比较重要。(【译注】比如:大部分都会将 js 放在 底部,css 放在顶部等。)