【译】浏览器如何呈现页面(二)

859 阅读5分钟

这篇文章是第二篇,第一篇在这里。

第一篇大致讲到了浏览器从获取原始数据开始,直到把内容画到屏幕上,但是还没有完。

之后,还有第三篇(整理)在这里

渲染阻塞

当你听到“渲染阻塞”的时候,你会想到啥?我猜是,‘某些行为阻止了浏览器把内容画到屏幕上’。

的确是你猜的那样。

所以这里有了我们的第一个优化点,把最重要的HTML内容和CSS样式提供给客户端,越快越好。

DOM和CSSOM都必须在屏幕绘制之前构造完成,所以HTMLCSS都是渲染阻塞资源。

所以关键点就是你应该让客户端竟可能快地获取你的htmlcss,这样就能够优化首屏渲染的时间。

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之后的脚本哈。)

重要1

因为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。

结果1

你有注意到这个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

结果2

为什么?

其实很简单。当HTML解析器正在创建DOM树的时候,发现有一个script标签,而此时,body标签以及它的内容还没有解析出来。DOM构建被暂停直到这个script执行完。

这里又带出了另一个重要的点。

你脚本的位置很重要。

重要2

如果你以文件形式加载一个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创建好。

重要3

所以当遇到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 放在顶部等。)

原文