JS 与 CSS 是否会阻塞 DOM 的渲染和解析呢?

目录
文章目录隐藏
  1. 准备工作
  2. CSS 不会阻塞 DOM 解析,但是会阻塞 DOM 渲染
  3. JS 会阻塞 DOM 解析
  4. CSS 会阻塞 JS 的执行
  5. JS 会触发页面渲染
  6. Body 内的 CSS
  7. 综上所述

最近系统梳理HTML5所有涉及到的标签时,梳理至<link><script>标签时,碰巧想到一个困扰很久的问题,即一般把<script>放在<body>尾部,<link>标签放在<head>内部,而页面通过CDN引入第三方框架或库时,基本都是将其<script>标签放在<link>标签前面。

可能此方式已经成为了约定俗成,但是究竟其好处在哪里,或者说其它的方式为什么不可取,想必你也和我有同样的疑问,那就接着来往下看吧。

准备工作

首先需要做的准备工作是,搭建一个服务器,目的是为了返回css样式和js脚本,并且让服务器根据传递的参数,固定延时返回数据。

其目录结构如下,其中index.jsstyle.css就是用于返回的数据,app.js为服务器启动文件,index.html是用来测试案例的文件,剩余文件或文件夹可以忽略。

├── static
│   ├── index.js
│   ├── style.css
├── app.js
├── index.html
├── package.json
├── node_modules/

涉及的相关代码也贴一下吧,方便复制调试。有必要说明一下,本地运行node app.js启动后,浏览器输入http://127.0.0.1:3000/就能访问到index.html,而访问style.css可以输入http://127.0.0.1:3000/static/style.css?sleep=3000,其中sleep参数则可自由控制css文件延时返回,例如想要文件5s后返回就设置sleep=5000

// app.js
const express = require('express')
const fs = require('fs')
const app = new express()
const port = 3000

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

const filter = (req, res, next) => {
    const { sleep } = req.query || 0

    if (sleep) {
        sleepFun(sleep).then(() => next())
    } else {
        next()
    }
}

app.use(filter)

app.use('/static/', express.static('./static/'))

app.get('/', function (req, res, next) {
    fs.readFile('./index.html', 'UTF-8', (err, data) => {
        if (err) return
        res.send(data)
    })
})

app.listen(port, () => {
    console.log(`app is running at http://127.0.0.1:${port}/`)
})

// static/index.js
var p = document.querySelector('p');
console.log(p);

// static/index.css
p { color: lightblue; }

接着就是index.html的准备工作,其中HTML部分的架子就长下面那样,然后你只需要记住DOMContentLoaded事件将在页面DOM解析完成后触发。

<!DOCTYPE html>
<html lang="zh-CN">

<head>
    <script>
        document.addEventListener('DOMContentLoaded', () => {
            var p = document.querySelector('p')
            console.log(p)
        })
    </script>
</head>

<body>
    <p>hello world</p>
</body>

</html>

CSS 不会阻塞 DOM 解析,但是会阻塞 DOM 渲染

首先在index.html插入如下<link>标签,然后在浏览器输入http://127.0.0.1:3000/访问此页面。

<head>
    <script>
        document.addEventListener('DOMContentLoaded', () => {
            var p = document.querySelector('p')
            console.log(p)
        })
    </script>
    <link rel="stylesheet" href="./static/style.css?sleep=3000">
</head>

<body>
    <p>hello world</p>
</body>

页面初始显示为空白,控制台打印出了p元素,同时浏览器标签页上加载loading3s后页面显示出浅蓝色的hello world

CSS 不会阻塞 DOM 解析,但是会阻塞 DOM 渲染
以上情况也就说明,CSS不会阻塞DOM的解析,如果说CSS阻塞DOM解析的话,那么p标签不会被解析,进而DOM不会被解析完成,CSS请求过程中也不可能会触发DOMContentLoaded事件。而且在css请求过程中,控制台立即打印出了p元素,由此也验证了此结论的正确性。

另一个情况就是,虽然DOM很早就被解析完成,但是p标签却迟迟没有渲染,原因在于CSS样式还未请求完成,在样式获取后hello world才被渲染出来,所以说CSS会阻塞页面渲染。

简单阐述一下浏览器的解析渲染过程,解析DOM生成DOM Tree,解析CSS生成CSSOM Tree,两者结合生成render tree渲染树,最后浏览器根据渲染树渲染至页面。由此可以看出DOM Tree的解析和CSSOM Tree的解析是互不影响的,两者是并行的。因此CSS不会阻塞页面DOM的解析,但是由于render tree的生成是依赖DOM TreeCSSOM Tree的,因此CSS必然会阻塞DOM的渲染。

更为严谨一点的说,CSS会阻塞render tree的生成,进而会阻塞DOM的渲染。

JS 会阻塞 DOM 解析

为了避免加载CSS造成的干扰,如下仅关注JS的执行情况,其中for循环的循环体中逻辑暂不考虑,仅仅是让JS执行更多时间。

<head>
    <script>
        document.addEventListener('DOMContentLoaded', () => {
            var p = document.querySelector('p')
            console.log(p)
        })
    </script>
</head>

<body>
    <script>
        const p = document.querySelector('p')
        console.log(p)
    
        for (var i = 0, arr = []; i < 100000000; i++) {
            arr.push(i)
        }
    </script>
    <p>hello world</p>
</body>

浏览器访问页面,初始时为空白且控制台打印null,浏览器loading短暂延时后,控制台打印出p标签同时页面渲染出hello world

JS 会阻塞 DOM 解析
以上情况很容易说明JS会阻塞DOM解析了,JS执行初控制台打印null,因为此时p标签还未被解析,for循环执行时,可以明显感觉到执行耗时,执行完成p标签被解析,此时触发DOMContentLoaded事件,控制台打印出p标签,同时页面渲染出hello world

比较合理的解释就是,首先浏览器无法知晓JS的具体内容,倘若先解析DOM,万一JS内部全部删除掉DOM,那么浏览器就白忙活了,所以就干脆暂停解析DOM,等到JS执行完成再继续解析。

CSS 会阻塞 JS 的执行

如下在页内JS脚本前插入<link>标签,并且延时3s获取CSS样式。

<head>
    <script>
        document.addEventListener('DOMContentLoaded', () => {
            var p = document.querySelector('p')
            console.log(p)
        })
    </script>
    <link rel="stylesheet" href="./static/style.css?sleep=3000">
    <script src="./static/index.js"></script>
</head>

<body>
    <p>hello world</p>
</body>

初始页面空白,浏览器loading加载3s后,控制台打印出null,紧接着打印出p标签,同时页面渲染出浅蓝色p标签。

CSS 会阻塞 JS 的执行
此情况好像是CSS不仅阻塞了DOM的解析,而且也阻塞了DOM渲染。

但是首先要思考下是什么阻塞了DOM的解析,刚刚已经证明了CSS不会阻塞DOM的解析,所以只可能是JS阻塞了DOM解析。但是JS只有两行代码,不会阻塞长达3s左右的时间。所以只有一个可能就是CSS会阻塞JS的执行。

因此输出结果也能大致分析出来了,首先解析到第一个<script>标签,document绑定上DOMContentLoaded事件,紧接着解析到link标签,浏览器请求CSS样式,由于CSS不会阻塞DOM解析,因此浏览器继续向下解析,发现第二个<script>标签,浏览器请求JS脚本,此时JS获取完成,但是由于CSS还在获取,所以不能立即执行。

而第二个<script>不能立即执行,导致它后面的p标签也没办法解析,原因则是JS会阻塞DOM解析。只有等待到CSS样式获取成功后,此时JS立即执行,控制台输出null,然后浏览器继续解析到p标签,解析完成,DOMContentLoaded事件触发,控制台输出p标签,最后浅蓝色hello world渲染至页面。

其实这样做也是有道理的,设想JS脚本中的内容是获取DOM元素的CSS样式属性,如果JS想要获取到DOM最新的正确的样式,势必需要所有的CSS加载完成,否则获取的样式可能是错误或者不是最新的。因此要等到JS脚本前面的CSS加载完成,JS才能再执行,并且不管JS脚本中是否获取DOM元素的样式,浏览器都要这样做。

回溯文章开头的那个疑问,所以一般将<script>放在<link>标签前面是有道理的。

JS 会触发页面渲染

如下CSS采用页内方式,其中颜色名及其rgb值分别为浅绿色lightbluergb(144, 238, 144))、粉色pinkrgb(255, 192, 203))。

// index.html
<head>
    <style>
        p {
            color: lightgreen;
        }
    </style>
</head>

<body>
    <p>hello</p>
    <script src="./static/index.js?sleep=2000"></script>
    <p>beautiful</p>
    <style>
        p {
            color: pink;
        }
    </style>
    <script src="./static/index.js?sleep=4000"></script>
    <p>world</p>
    <style>
        p {
            color: lightblue;
        }
    </style>
</body>

// static/index.js
var p = document.querySelector('p');
var style = window.getComputedStyle(p, null);
console.log(style.color);

页面初始渲染出浅绿色hello,紧接着2s后渲染出粉色hello beautiful且控制台打印rgb(144, 238, 144),然后又2s后渲染出浅蓝色hello beautiful world且控制台打印rgb(255, 192, 203)

JS 会触发页面渲染
上述结果大致分析为浏览器首先解析第一个<style>标签和hello文本的p标签,此时继续向下解析发现了第一个<script>标签,紧接着触发一次渲染,由于此过程非常快所以页面初始就能看到浅绿色hello

然后浏览器发出JS请求,2sJS获取完成立即运行控制台输出rgb(144, 238, 144)JS运行完成后浏览器继续向下解析到beautiful文本的p标签和第二个<style>标签,再继续向下解析发现了第二个<script>标签,触发一次渲染,这个过程也是非常快,所以可以看到控制台输出结果和渲染粉色hello beautiful几乎是同时的。

解析到第二个<script>标签时,浏览器不会发出请求(稍作解释),2s后获取到JS脚本并执行,控制台输出rgb(255, 192, 203),紧接着浏览器继续向下解析到world文本的p标签和第三个<style>标签,此时DOM解析完成,再进行正常的渲染,这个过程也是非常快,所以也能看到控制台输出结果和渲染浅蓝色hello beautiful world几乎是同时的。

现在来解答刚才那个问题,浏览器解析DOM时,虽然会一行一行向下解析,但是它会预先加载具有引用标记的外部资源(例如带有src标记的<script>标签),而在解析到此标签时,则无需再去加载,直接运行,以此提高运行效率。所以就会有上述两个输出结果间隔2s的情况,而不是4s,因为浏览器预先就一起加载了两个<script>脚本,第一个<script>脚本加载完成时,第二个<script>脚本还剩大概2s加载完成。

而这个结论才是解释为何CSS会阻塞JS的执行的真正原因,浏览器无法预先知道脚本的具体内容,因此在碰到<script>标签时,只好先渲染一次页面,确保<script>脚本内能获取到DOM的最新的样式。倘若在决定渲染页面时,还有尚未加载完成的CSS样式,只能等待其加载完成再去渲染页面。

Body 内的 CSS

来看一个较为特殊的情况。

<head>
    <script>
        document.addEventListener('DOMContentLoaded', () => {
            var p = document.querySelector('p')
            console.log(p)
        })
    </script>
</head>

<body>
    <p>hello</p>
    <link rel="stylesheet" href="./static/style.css?sleep=3000">
    <p>world</p>
</body>

按照上述的所有结论,预先分析一下运行结果,首先浏览器解析<script>脚本,document上绑定了DOMContentLoaded事件,紧接着浏览器继续向下解析,发现了文本为hellop标签和<link>标签,浏览器发起CSS请求,由于CSS不会阻塞DOM解析,浏览器继续向下解析至文本为worldp标签,此时页面解析完成,DOMContentLoaded事件触发控制台输出p标签,3s后页面渲染出浅蓝色hello world

但是实际结果并不是这样,而是页面初始就渲染出hello,3s后页面渲染出浅蓝色hello world并且打印p标签。

Body 内的 CSS
如下是我个人的分析和理解,首先是浏览器解析并运行<script>标签,然后在解析文本为hellop标签,当解析到<link>标签时,触发一次渲染,然后浏览器发起CSS请求,但是此时浏览器不会继续向下解析,而是将<link>标签当做是DOM的一部分,换句话说浏览器将其认为是特殊的DOM元素,这个DOM元素的特殊性就在于需要进行加载,因此浏览器不会继续向下解析,所以也就没有DOMContentLoaded的输出结果。

3s<link>这个特殊的DOM元素解析完成,浏览器继续向下解析world文本的p标签,此时触发DOMContentLoaded事件,再进行正常的渲染,页面渲染出浅蓝色hello world,由于此过程非常快,所以控制台输出和渲染浅蓝色hello world几乎是同时的。

上述仅仅是我个人的分析和猜测,可以不必理会,仅作为讨论,所以也不敢妄下结论,误人子弟,此小节仅走马观花即可。

综上所述

综合上述所有情况,可以得出如下结论。

  • CSS不会阻塞DOM解析,但是会阻塞DOM渲染,严谨一点则是CSS会阻塞render tree的生成,进而会阻塞DOM的渲染
  • JS会阻塞DOM解析
  • CSS会阻塞JS的执行
  • 浏览器遇到<script>标签且没有deferasync属性时会触发页面渲染
  • Body内部的外链CSS较为特殊,请慎用

「点点赞赏,手留余香」

0

给作者打赏,鼓励TA抓紧创作!

微信微信 支付宝支付宝

还没有人赞赏,快来当第一个赞赏的人吧!

声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。
码云笔记 » JS 与 CSS 是否会阻塞 DOM 的渲染和解析呢?

发表回复