JS 和 CSS 的位置对资源加载顺序的影响

2017-01-19 Javascript

索引[隐藏]

原文地址

之前我一直在困扰这样的问题:JS 和 CSS 的位置对资源加载顺序的影响,最近在网上查阅了一些资料并且通过自己的测试后,得出的结论:

用一句话概括就是: JS 全阻塞,CSS 半阻塞。(词是我发明的,方便记忆而已)

  1. JS 会阻塞后续 DOM 解析以及其它资源(如 CSS,JS 或图片资源)的加载。
  2. CSS 不会阻塞后续 DOM 结构的解析,但会阻塞 DOM 的渲染,不会阻塞其它资源(如图片)的加载,但是会阻塞 JS 文件的加载。
  3. 现代浏览器很聪明,会进行 prefetch 优化,浏览器在获得 html 文档之后会对页面上引用的资源进行提前下载。(注意仅仅只是提前下载)

下面开始我就一边测试,一边解释上述测试的结果:

测试的浏览器是 Chrome,版本号为 55.0.2883.95 (64-bit)

先用 Nodejs 搭建一个简单 http 服务器:

//test.js
const http = require('http');
const fs = require('fs');
const hostname = '127.0.0.1';
const port = 9000;

http.createServer((req, res) => {
    if(req.url === "/") {
        fs.readFile("index.html", "utf-8", function(err, data) {
            res.writeHead(200, { 'Content-Type': 'text/html' });
         res.write(data);
         res.end();    
    })
    }else if(req.url === "/yellow.js") {
    //延迟 5s
    fs.readFile("yellow.js", "utf-8", function(err, data) {
        res.writeHead(200, { 'Content-Type': 'text/plain' });
        setTimeout(function () {
            res.write(data);
            res.end();
        }, 5000);
    })
    }else if(req.url === "/blue.js") {
    //延迟 10s
    fs.readFile("blue.js", "utf-8", function(err, data) {
        res.writeHead(200, { 'Content-Type': 'text/plain' });
        setTimeout(function () {
            res.write(data);
            res.end();
        }, 10000);
    })
    }else if(req.url === "/red.css") {
    //延迟 15s
    fs.readFile("red.css", "utf-8", function(err, data) {
        res.writeHead(200, { 'Content-Type': 'text/css' });
        setTimeout(function () {
            res.write(data);
            res.end();
        }, 15000);
    })
    }else if(req.url === "/green.css") {
    //延迟 20s
    fs.readFile("green.css", "utf-8", function(err, data) {
        res.writeHead(200, { 'Content-Type': 'text/css' });
        setTimeout(function () {
            res.write(data);
            res.end();
        }, 20000);
    })
    }

}).listen(port, hostname, () => {
  console.log('Server running at ' + hostname);
});

首页的代码结构:

//index.html
<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <meta http-equiv="cache-control" content="no-cache,no-store, must-revalidate" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <title>测试浏览器渲染</title>
</head>
<body>
    <p>First Line</p>
    <script src="/yellow.js"></script>
    <p>Second Line</p>
    <link rel="stylesheet" href="/red.css">
    <p>Third Line</p>
    <script src="/blue.js"></script>
    <p>Fourth Line</p>
    <link rel="stylesheet" href="/green.css">
    <img src="http://www.liuhaihua.cn/wp-content/uploads/2016/02/uuUFNjm.png">
    <p>Fifth Line</p>
</body>
</html>

以及其它 CSS 和 JS 文件:

//yellow.js
document.body.style.cssText = "background: yellow !important";

//blue.js
document.body.style.cssText = "background: blue !important";
//red.css
body {
    background:red !important;
}

//green.css
body {
    background: green !important;
}

说明下:yellow.js 和 blue.js 下载时间分别为 5s 和 10s,red.css 和 green.css 下载时间分别为 15s 和 20s。

之后将所有文件放到同个目录下,在控制台输入 node test.js,打开浏览器访问 127.0.0.1:9000 就可以访问。

现代浏览器很聪明,会进行 prefetch 优化,浏览器在获得 html 文档之后会对页面上引用的资源进行提前下载。(注意仅仅只是提前下载)

先来看第三点结论:

很好理解,从图中可以看出:CSS、JS、图片在浏览器在拿到 html 文档之后会将页面上引用资源几乎同时下载,但具体什么时候执行要看 html 的结构,注意我这里使用的是 Chrome 浏览器,其它浏览器可能会有差别。

还有就是一个奇怪的现象,Chrome 浏览器有时会对 img 进行 prefetch,有时则不会。

JS 会阻塞后续 DOM 解析以及其它资源(如 CSS,JS 或图片资源)的加载

接着是第一点规则:

从上图可以看出,当浏览器解析到 yellow.js 这行时候会等待 yellow.js 加载,阻塞后续 DOM 结构的解析(包括 DOM 结构,其他所有资源(CSS, JS, 图片))。

这个很好理解:

  1. JS 运行在浏览器中,是单线程的,每个 window 一个 JS 线程,所以当然会阻塞后续 DOM 树的解析咯。
  2. JS 有可能会修改 DOM 结构,给 DOM 添加样式等等,所以这就意味着在当前 JS 加载执行完成前,后续资源的加载可能是没有意义的。

CSS 不会阻塞后续 DOM 结构的解析,不会阻塞其它资源(如图片)的加载,但是会阻塞 JS 文件的加载

其次第二点:

这个就相对比较复杂点,让我先上测试结果的图:

从图中可以得出以下总结:

在加载完 yellow.js 后,当在下载 red.css 时候并不会阻塞 DOM 解析,并且由于第一点规则,当解析到 blue.js 这行的时候,同样会阻塞后续 DOM 解析。 由于我们设置的 red.css 下载时间为 15s 而 blue.js 为 10s,而从前面第三条规则的图中也可以看到,blue.js 在 10s 左右下载完而 red.css 在 15s 左右下载完毕。 最后在 15s 时候页面变为了蓝色,这说明了 CSS 阻塞了 JS 的加载,后续的 JS 文件虽然提前下载完毕了,但还是要等前面 CSS 文件加载完后才能执行。 后续当 blue.js 加载完之后可以看到,green.css 的下载并不会影响到后续 img 的加载,所以说明 CSS 文件下载并不会影响后续图片等其它资源以及 DOM 的加载。

这个也好理解:JS 代码在执行前,浏览器必须保证在 JS 之前的所有 CSS 样式都解析完成,不然不就乱套了,前面的 CSS 样式可能会覆盖 JS 文件中定义的元素样式,这是 CSS 阻塞后续 JS 执行的根本原因。

最后这里说明下为什么最后 body 的背景色没有变成绿色:因为 js 定义的样式在内联样式,优先级高于在 CSS 文件中定义的样式,所以不是 gr`een.css 没有加载,而是没有生效。看下图就知道了:(green 和 red 样式都被划掉了)

所以知道了上述的结论之后,我们在开发的时候应该尽可能地:

  1. 将样式或 CSS 文件定义在 head 中,并且在处理此类请求的时候应该尽快能够响应(CDN 什么的),如果像上面请求一个 CSS 文件都要 10s 的话,那你这页面估计没多少人有耐心等下去。
  2. 将 JS 脚本文件放在 body 底部,让 DOM 结构能优先渲染出来,避免 DOM 被阻塞。
  3. 当编写比较耗时的 JS 代码时候尽可能使用异步的方式进行加载,比如 setTimeout, ajax 等等,同样也是为了避免页面渲染耗时过长,影响用户体验。

其他:

上面介绍了 JS 会阻塞后续 DOM 解析以及其它资源(如 CSS,JS 或图片资源)的加载,这是在没有考虑到 defer, async 的情况下。

当浏览器碰到 script 脚本的时候:(不考虑浏览器的 prefetch)

  1. 没有 defer 或 async,浏览器会立即加载并执行指定的脚本,“立即”指的是在渲染该 script 标签之下的文档元素之前,也就是说不等待后续载入的文档元素,当然还得等待前面的 CSS 文件渲染完。
  2. <script async src="script.js"></script> 有 async,加载和渲染后续文档元素的过程将和 script.js 的加载与执行并行进行(下载异步,执行同步)。
  3. <script defer src="script.js"></script> 有 defer,加载后续文档元素的过程将和 script.js 的加载并行进行(异步),但是 script.js 的执行要在所有元素解析完成之后,DOMContentLoaded 事件触发之前完成。

从使用的角度来看,首先把脚本丢到 body 底部是比较好的优化选择,此法可保证非脚本的其他一切元素能够以最快的速度得到加载和解析。

上述的三点用图可表示为:

蓝色线代表网络读取,红色线代表执行时间,这俩都是针对脚本的;绿色线代表 HTML 解析。

总结:

  1. 由于现代浏览器都存在 prefetch,所以 defer, async 可能并没有太多的用途,可以作为了解扩展知识,仅仅将脚本文件放到 body 底部就可以起到很不错的优化效果。
  2. defer 和 async 都是异步加载脚本文件。
  3. 慎用 async,因为它完全不考虑依赖关系,只要下载完后就加载,不考虑此时页面样式先后的加载顺序,不过它对于那些可以不依赖任何脚本或不被任何脚本依赖的脚本来说却是非常合适的,最典型的例子:Google Analytics。

0 条评论

来做第一个留言的人吧!

发表评论

邮箱地址不会被公开, 收到回复会通过邮件提醒。


TOP