浅谈JS和CSS是如何阻塞DOM

  • Post author:
  • Post category:其他




后端Express

后端Node的代码,这里用的是Express框架

let express = require('express'); 
let app = new express(); 

let filter = (req, res, next) => {
    console.log(req.query);
    let {sleep} = req.query;
    if(sleep){
        sleepFun(sleep).then(()=> next());
    }else{
        next();
    }

};

app.use(filter);
app.use('/static', express.static('public'));

app.listen(3000,"127.0.0.1");

function sleepFun(time) {
    return new Promise(function(res) {
        setTimeout(() => {
            res()
        }, time);
    })
}

根据传入sleep的参数设置返回文件的时间。



CSS 阻塞页面渲染

<!DOCTYPE html>
<html lang="en">
<head>
    <link rel="stylesheet" href="css/common.css?sleep=3000">
    <meta charset="UTF-8">
    <title>hello</title>
    <style>
        div {
            width: 100px;
            height: 100px;
            background: lightgreen;
        }
    </style>
</head>
<body>
<div></div>
</body>
</html>

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>hello</title>
    <style>
        div {
            width: 100px;
            height: 100px;
            background: lightgreen;
        }
    </style>
    <link rel="stylesheet" href="css/common.css?sleep=3000">
</head>
<body>
<div></div>
</body>
</html>

我们把common.css放在html中的任何地方。

common.css中的内容是把背景设置为蓝色

div {
    background: lightblue;
}

第一种情况如果common.css是放在前面,那么最后渲染出来的是绿色,而且页面是等待3秒之后才出现绿色。

访问:http://127.0.0.1:3000/static/index.html

在这里插入图片描述

第二种情况如果是common.css是放在后面,那么最后渲染出来的是蓝色,而且页面也是等待3秒之后才出现蓝色。

访问:http://127.0.0.1:3000/static/index1.html

在这里插入图片描述

如果CSS 不会阻塞页面阻塞渲染,那么CSS文件下载之前,浏览器就会渲染出一个浅绿色的div,之后再变成浅蓝色,或者从浅蓝色变成浅绿色。浏览器的这个策略其实很明智的,想象一下,如果没有这个策略,页面首先会呈现出一个原始的模样,待CSS下载完之后又突然变了一个模样。用户体验可谓极差,而且渲染是有成本的。因此,基于性能与用户体验的考虑,浏览器会尽量减少渲染的次数,CSS顺理成章地阻塞页面渲染。



CSS 不会阻塞 DOM 的解析

关于CSS,

<link>

标签放在头部性能会高一点,如果

<script>



<link>

同时在头部的话,

<script>

在上可能会更好。这是为什么呢?下面我们一起来看一下CSS对DOM的影响是什么。

这里说的是DOM 解析,证明的例子如下,首先在头部插入

<script defer src="js/console.js"></script>

,JS文件的内容是:

<!DOCTYPE html>
<html lang="en">
<head>
    <link rel="stylesheet" href="css/common.css?sleep=3000">
    <script defer src="js/console.js"></script>
    <meta charset="UTF-8">
    <title>hello</title>
    <style>
        div {
            width: 100px;
            height: 100px;
            background: lightgreen;
        }
    </style>
</head>
<body>
<div></div>
</body>
</html>

  • console.js
console.log(document.querySelector('div'));

这里简单解释下defer模式和async模式

1.默认引用

script:<script type="text/javascript" src="x.min.js"></script>


当浏览器遇到 script 标签时,文档的解析将停止,并立即下载并执行脚本,脚本执行完毕后将继续解析文档。

2.async模式

<script type="text/javascript" src="x.min.js" async="async"></script>


当浏览器遇到 script 标签时,文档的解析不会停止,其他线程将下载脚本,脚本下载完成后开始执行脚本,脚本执行的过程中文档将停止解析,直到脚本执行完毕。

3.defer模式

<script type="text/javascript" src="x.min.js" defer="defer"></script>


当浏览器遇到 script 标签时,文档的解析不会停止,其他线程将下载脚本,待到文档解析完成,脚本才会执行。

defer属性是用来通知浏览器该脚本将在文档完成解析后,触发 DOMContentLoaded 事件前执行。设置这个属性,能保证DOM解析后马上打印出div。

访问:http://127.0.0.1:3000/static/index2.html

在这里插入图片描述

可以看到是首先打印出div这个DOM节点,过3s左右之后才渲染出一个浅绿色的div。这就证明了CSS 是不会阻塞 DOM 的解析的,尽管CSS下载需要3s,但这个过程中,浏览器不会傻等着CSS下载完,而是会解析DOM的。



什么是DOMContentLoaded?

DOMContentLoaded事件触发表示当前的HTML文档已被完全加载和解析,

DOMContentLoaded事件本身不会等待CSS文件、图片、iframe加载完成。具体的定义可以参考:https://developer.mozilla.org/en-US/docs/Web/API/Window/DOMContentLoaded_event



DOMContentLoaded什么时候触发?

<!DOCTYPE html>
<html lang="en">
<head>
    <script>
        document.addEventListener("DOMContentLoaded",function () {
            console.log("DOMContentLoaded" + new Date())
        });
    </script>
    <script src="js/date.js"></script>
    <meta charset="UTF-8">
    <title> DOMContentLoaded什么时候触发?</title>
    <style>
        div {
            width: 100px;
            height: 100px;
            background: lightgreen;
        }
    </style>
    <link rel="stylesheet" href="css/common.css?sleep=3000">
</head>
<body>
<div></div>
</body>
</html>

  • date.js
console.log(new Date());

  • common.css
div {
    background: lightblue;
}

在代码中我们监听了DOMContentLoaded事件,然后在下面引入了date.js外部js文件,然后在最下面还引入了外部的css样式,而且下载时间需要3秒。

然后我们访问页面可以发现,date.js先打印出来时间,然后触发了DOMContentLoaded,此时页面还是空白的,过了3秒之后才出现浅蓝色的div。

访问:http://127.0.0.1:3000/static/index5.html

在这里插入图片描述

通过这段代码,我们可以知道DOMContentLoaded事件本身不会等待CSS文件加载。

所以总结一下:

DOMContentLoaded事件本身不会等待CSS文件、图片、iframe加载完成。

  1. 如果页面中没有script标签,DOMContentLoaded事件并没有等待CSS文件、图片加载完成。
  2. 如果页面中静态的写有script标签,DOMContentLoaded事件需要等待JS执行完才触发。而且script标签中的JS需要等待位于其前面的CSS的加载完成。

总的来说,当文档中没有脚本时,浏览器解析完文档便能触发 DOMContentLoaded 事件;如果文档中包含脚本,则脚本会阻塞文档的解析,而脚本需要等 CSSOM 构建完成才能执行。在任何情况下,DOMContentLoaded 的触发不需要等待图片等其他资源加载完成。



defer 与 DOMContentLoaded

如果 script 标签中包含 defer,那么这一块脚本将不会影响 HTML 文档的解析,而是等到 HTML 解析完成后才会执行。而 DOMContentLoaded 只有在 defer 脚本执行结束后才会被触发。 所以这意味着什么呢?HTML 文档解析不受影响,等 DOM 构建完成之后 defer 脚本执行,但脚本执行之前需要等待 CSSOM 构建完成。在 DOM、CSSOM 构建完毕,defer 脚本执行完成之后,DOMContentLoaded 事件触发。



async 与 DOMContentLoaded

如果 script 标签中包含 async,则 HTML 文档构建不受影响,解析完毕后,DOMContentLoaded 触发,而不需要等待 async 脚本执行、样式表加载等等。

引用:

你不知道的 DOMContentLoaded



浏览器渲染原理

HTML 文档被加载和解析完成之前,浏览器做了哪些事情呢?

浏览器向服务器请求到了 HTML 文档后便开始解析,产物是 DOM(文档对象模型),到这里 HTML 文档就被加载和解析完成了。如果有 CSS 的会根据 CSS 生成 CSSOM(CSS 对象模型),然后再由 DOM 和 CSSOM 合并产生渲染树。有了渲染树,知道了所有节点的样式,下面便根据这些节点以及样式计算它们在浏览器中确切的大小和位置,这就是布局阶段。有了以上这些信息,下面就把节点绘制到浏览器上。所有的过程如下图所示:

[外链图片)(221F207EE4964FDD96AAF2C63431DC58)]



CSS 阻止JS执行

我们来看看下面这段的代码,和上面不同的是这里的

<script>

并没有加defer

<!DOCTYPE html>
<html lang="en">
<head>
    <link rel="stylesheet" href="css/common.css?sleep=3000">
    <script src="js/console.js"></script>
    <meta charset="UTF-8">
    <title> CSS 阻止JS执行</title>
    <style>
        div {
            width: 100px;
            height: 100px;
            background: lightgreen;
        }
    </style>
</head>
<body>
<div></div>
</body>
</html>

访问:http://127.0.0.1:3000/static/index3.html

[外链图片)(70B729371F80484CBCE2F69C662660FD)]

可以看到页面等待了3秒,但此过程中不会打印任何东西,之后呈现出一个浅绿色的div,再打印出null。

看起来好像是CSS阻塞了DOM的解析。但是前面我们已经了解了CSS是不会阻塞DOM的,那么阻塞DOM的到底是什么?其实阻塞页面解析的其实是JS,因为JS在等待CSS的下载,等待CSS完成之后才执行JS。这是为什么呢?

仔细思考一下,其实这样做是有道理的,如果脚本的内容是获取元素的样式,宽高等CSS控制的属性,浏览器是需要计算的,也就是依赖于CSS。浏览器也无法感知脚本内容到底是什么,为避免样式获取,因而只好等前面所有的样式下载完后,再执行JS

所以,为何

<script>



<link>

同时在头部的话,

<script>

在上可能会更好, 之所以是可能,是因为如果的内容下载更快的话,是没影响的,但反过来的话,JS就要等待了,然而这些等待的时间是完全不必要的。



JS 阻塞 DOM 解析

const arr = [];
for (let i = 0; i < 10000000; i++) {
    arr.push(i);
    arr.splice(i % 3, i % 7, i % 5);
}
const div = document.querySelector('div');
console.log(div);

我写着一段代码是为了让js文件执行的更加久一些。

const arr = [];
for (let i = 0; i < 10000000; i++) {
    arr.push(i);
    arr.splice(i % 3, i % 7, i % 5);
}
const div = document.querySelector('div');
console.log(div);

然后我们访问以下页面,可以看到浏览器在加载,页面空白,然后打印出null,最后才出现浅绿色的div。这就说明了JS是阻塞DOM的解析。原因也很简单 ,浏览器预知js文件里有什么内容,万一JS脚本内删除了后面即将解析的DOM元素,那么浏览器就白干活了。所以浏览器会等待脚本执行完毕之后才继续解析后面的DOM。

访问:http://127.0.0.1:3000/static/index4.html

[外链图片)(BB68F5AF410E44E882A81BB3B1EB3FFE)]

所以总结一下就是JavaScript 可以阻塞 DOM 的生成,也就是说当浏览器在解析 HTML 文档时,如果遇到

<script>

,便会停下对 HTML 文档的解析,转而去处理脚本。如果脚本是内联的,浏览器会先去执行这段内联的脚本,如果是外链的,那么先会去加载脚本,然后执行。在处理完脚本之后,浏览器便继续解析 HTML 文档。



浏览器遇到 script标签时,会触发页面渲染

其实这才是解释上面为何JS执行会等待CSS下载的原因

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title> 浏览器遇到script标签时,会触发页面渲染</title>
    <style>
        div {
            width: 100px;
            height: 100px;
            background: lightgreen;
        }
    </style>
</head>
<body>
    <div></div>
    <script src="js/console.js?sleep=3000"></script>
    <style>
        div {
            background: lightgrey;
        }
    </style>
    <script src="js/console.js?sleep=5000"></script>
    <link rel="stylesheet" href="css/common.css">
</body>
</html>

我们可以在浏览器上访问,可以看到页面先是显示浅绿色的div,然后在现实浅灰色,最后才是浅蓝色。

访问:http://127.0.0.1:3000/static/index6.html

在这里插入图片描述

由此可见,每次碰到

<script>

标签时,浏览器都会渲染一次页面。这是基于同样的理由,浏览器不知道脚本的内容,有可能在js脚本中需要获取DOM元素的宽高或者CSS样式,因而碰到脚本时,只好先渲染页面,确保脚本能获取到最新的DOM元素信息,尽管脚本可能不需要这些信息。



总结

综上所述,我们得出这样的结论:

  • CSS 不会阻塞 DOM 的解析,
  • CSS 会阻塞 DOM 渲染。
  • JS 阻塞 DOM 解析
  • 浏览器遇到

    <script>

    且没有defer或async属性的 标签时,会触发页面渲染,因而如果前面CSS资源尚未加载完毕时,浏览器会等待它加载完毕在执行脚本。

所以,

<script>

最好放底部,

<link>

最好放头部,如果头部同时有

<script>



<link>

的情况下,最好将

<script>

放在

<link>

上面



参考

参考:


原来 CSS 与 JS 是这样阻塞 DOM 解析和渲染的


再谈DOMContentLoaded与渲染阻塞—分析html页面事件与资源加载


脚本引用中的DEFER和ASYNC的用法和区别


你不知道的 DOMContentLoaded



源代码

https://gitee.com/cckevincyh/js-css-block-dom



版权声明:本文为cckevincyh原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。