网页加载与阻塞

前言

注:本文写于2020年较早的时候,只是最近整理发布,因此关于渲染流程的认识比较肤浅,重点关注阻塞策略部分就好了;

基本流程

img

相关概念

  • DOMDocument Object Model,即文档对象模型;用于描述文档的结构及元素属性,是HTML和XML文档的编程接口(和JS进行交互的接口)。

  • CSSOMCSS Object Model,即CSS对象模型;跟DOM类似,不过CSSOM是用来描述样式的结构和属性,是CSS的编程接口(和JS进行交互的接口)。

  • Render Tree:是DOMCSSOM两者的结构,渲染树即包含可以被渲染的元素节点,同时也包含对应节点的样式信息;

    哪些节点是不会被解析到渲染树中?

    • 不包括headscriptmeta等不可见的节点;
    • 某些通过CSS隐藏的节点在渲染树中也会被忽略,比如应用了display:none 规则的节点(visibility: hidden只是视觉不可见,仍占据空间,不会被忽略)[1]
  • Layout:布局;根据渲染树进一步计算出每个渲染节点的位置和尺寸信息(对应顶点着色器的工作?);

  • Paint:绘制;根据布局信息,得出渲染节点的像素信息,最终通过显卡绘制到屏幕的相应区域(对应片元着色器的工作?);

  • 回流reflow,也叫重排;该流程会重新计算文档中元素的布局信息,某些对DOMCSS的修改就会引发回流;

  • 重绘repaint,即重新绘制部分或全部文档元素;当元素的像素信息改变时会触发重绘,很明显,回流必定引发重绘;

阻塞策略

虽然普通的linkscript会阻塞DOM的解析,但是有一些方法可以调整原始的阻塞策略;

调整script位置

由于script不论内联还是外部,不论加载与否都会阻塞DOM的解析,因此将script标签移至DOM最后(即<body>闭合标签前),可以最小地影响DOM的解析。

defer/async属性

这两个可以用于非内联script标签(即必须设置src属性),当设置为true时,对应的script标签不会阻塞DOM的解析,但是两个属性的作用还是有所不同的。

defer属性

defer,意为延迟;该属性除了可以使script标签不阻塞DOM解析,还会使对应的script标签的执行阶段延迟到DOM解析完成后,DOMContentLoaded事件触发前;也就是所有的defer脚本都必须在加载完且DOM解析完成后才能执行,且按照顺序执行;

async属性

这个属性就比较简单粗暴,仅仅只是不阻塞DOM而已,加载完后对应的script会按顺序执行,因此执行的时机取决于加载完成的时间点(有可能在DOMContentLoaded事件触发前执行,也可能在触发后执行),但是必定会在windowload事件触发前执行;

预加载策略

预加载,顾名思义就是预先加载某些会用到的文件,可以是脚本、样式,也可以是媒体文件等;通过预加载设置可以改变原本的一些文件加载顺序和优先级;

设置link标签的rel属性为preloadprefetch可以开启对应文件的预加载,此时href属性值就是预加载的文件地址,然后通过as属性可以指定文件用途(浏览器内部根据不同的as类型会有不同的加载优先级);

preload 还有许多其他好处。使用 as来指定将要预加载的内容的类型,将使得浏览器能够:

  • 更精确地优化资源加载优先级。
  • 匹配未来的加载需求,在适当的情况下,重复利用同一资源。
  • 为资源应用正确的内容安全策略。
  • 为资源设置正确的 Accept 请求头。[2]

比如,可以通过设置对应的预加载,提前在头部加载脚本文件,然后在底部执行脚本;这样既能避免阻塞DOM解析,又可以提前加载脚本文件;

1
2
3
4
5
6
7
8
9
<head>
<!-- ... -->
<link href="test.js" rel="preload" as="script">
<!-- ... -->
</head>
<body>
<!-- ... -->
<script src="test.js"></script>
</body>

preload和prefetch的区别

prefetch虽然也是对资源文件进行预加载,但是其设计目的是为了预加载下一页或之后可能会跳转的页面所需要用到的文件,因此其优先级低于preload

验证实验

  1. script的执行是否阻塞DOM的解析?

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    <p id="t1">t1</p>
    <script>
    for (let i = 1; i < 1000000000; i++) {
    // 模拟同步执行耗时
    }
    console.log(1, window.t2)
    </script>
    <p id="t2">t2</p>
    <script>
    console.log(2, window.t2)
    </script>

    根据上述代码设计,如果script的执行不会阻塞DOM解析,那么第一处打印应该不会是undefined;反之若阻塞DOM解析,则为undefined;实际结果为:

    img

    因此答案为:script的执行会阻塞DOM的解析;

  2. script的加载是否阻塞DOM的解析?

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    <head>
    <script>
    let start = Date.now()
    </script>
    <script>
    setTimeout(() => {
    console.log(1, window.t1)
    }, 10)
    </script>
    <script src="https://cdn.bootcdn.net/ajax/libs/vue/2.6.11/vue.js"></script>
    </head>
    <body>
    <p id="t1">t1</p>
    <script>
    console.log(Date.now() - start)
    console.log(2, window.t1)
    </script>
    </body>

    根据上述设计,如果外部script的加载不会阻塞DOM解析,那么第一处的打印将不会是undefined;反之则为undefined;测试的时候可以将网速调慢,这样对比更明显:

    img

    得到的测试结果为:

    img

    因此答案为:script的加载会阻塞DOM的解析;

  3. css的加载是否阻塞DOM的解析?

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    <head>
    <script>
    let start = Date.now()
    </script>
    <script>
    setTimeout(() => {
    console.log(1, window.t1)
    }, 100)
    </script>
    <link href="https://cdn.bootcdn.net/ajax/libs/twitter-bootstrap/4.5.0/css/bootstrap-grid.css" rel="stylesheet">
    </head>
    <body>
    <p id="t1">t1</p>
    <script>
    console.log(Date.now() - start)
    console.log(2, window.t1)
    </script>
    </body>

    同理,首先降低网速,得到的打印结果为:

    img

    因此答案为:css的加载不会阻塞DOM的解析;

  4. css的加载是否阻塞JS的执行?

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    <head>
    <script>
    let start = Date.now()
    </script>
    <link href="https://cdn.bootcdn.net/ajax/libs/twitter-bootstrap/4.5.0/css/bootstrap-grid.css" rel="stylesheet">
    <script>
    console.log(Date.now() - start)
    console.log(1, window.t1)
    </script>
    </head>
    <body>
    <p id="t1">t1</p>
    </body>

    同样地,降低网速进行测试,得到打印结果:

    img

    显然,css文件之后的js是在css文件加载之后才执行的;

    因此答案为:css的加载会阻塞JS的执行;

  5. css的加载是否阻塞DOM的渲染?

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    <head>
    <style>
    #t1 {
    color: red;
    }
    </style>
    <script>
    let start = Date.now()
    console.log('start')
    setTimeout(() => {
    console.log(1, window.t1)
    }, 10)
    </script>
    <link href="https://cdn.bootcdn.net/ajax/libs/twitter-bootstrap/4.5.0/css/bootstrap.css" rel="stylesheet">
    </head>
    <body>
    <p id="t1">t1</p>
    <script>
    console.log(Date.now() - start)
    </script>
    </body>

    同样地,降低网速进行测试,录制结果为:

    img

    从录制gif可以明显看到虽然css加载不阻塞DOM的解析,但是加载过程中DOM是不会被渲染到浏览器的,直到css文件加载完成(CSSOM构建完成)才会进行渲染,因此css的加载会阻塞DOM的渲染,造成白屏;

  6. 设置deferscript加载是否阻塞DOM的解析?

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    <head>
    <script>
    let start = Date.now()
    </script>
    <script>
    setTimeout(() => {
    console.log(1, window.t1)
    }, 0)
    </script>
    <script src="./test06.js" defer></script>
    </head>
    <body>
    <p id="t1">t1</p>
    <script>
    document.addEventListener('DOMContentLoaded', () => {
    console.log(Date.now() - start)
    console.log('DOMContentLoaded', window.t1)
    })
    </script>
    </body>
    1
    2
    // test06.js
    console.log('外部JS')

    同样地,降低网速进行测试,得到打印结果:

    img

    从打印结果不难看出加了defer属性的script加载并不会阻塞DOM的解析,而且会在DOMContentLoaded事件之前执行完;

  7. 设置asyncscript加载是否阻塞DOM的解析?

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    <head>
    <script>
    let start = Date.now()
    </script>
    <script>
    setTimeout(() => {
    console.log(1, window.t1)
    }, 0)
    </script>
    <script src="./test07.js" async></script>
    </head>
    <body>
    <p id="t1">t1</p>
    <script>
    document.addEventListener('DOMContentLoaded', () => {
    console.log(Date.now() - start)
    console.log('DOMContentLoaded', window.t1)
    })
    window.addEventListener('load', () => {
    console.log('load')
    })
    </script>
    </body>

    同样地,降低网速进行测试,得到打印结果:

    img

    结果跟defer类似,不会阻塞DOM的解析,而且async脚本并不会影响到DOMContentLoaded事件的触发,但是会影响到load事件的触发;

  8. 进行预加载(preload)的script是否阻塞DOM的解析?

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    <head>
    <script>
    let start = Date.now()
    </script>
    <script>
    setTimeout(() => {
    console.log(1, window.t1)
    }, 0)
    </script>
    <link href="./test08.js" rel="preload" as="script">
    </head>
    <body>
    <p id="t1">t1</p>
    <script src="./test08.js"></script>
    </body>

    同样地,降低网速进行测试,得到打印结果:

    img

    结果很明显:预加载的script不会阻塞DOM的解析;

    但是,如果预加载的script后面有其他DOM,这时仍然会阻塞后面的DOM解析!

上述实验源码放在这里:https://github.com/xxf1996/web-load-test

实验结论

根据以上实验测试,可以得出:

  1. css的加载会阻塞其后的js执行,不会阻塞DOM的解析,但是会阻塞DOM的渲染;
  2. js的加载和执行都会阻塞DOM的解析;
  3. script设置deferasync属性可以避免阻塞DOM的解析;

相关文档


  1. 页面渲染:过程分析 - 掘金 ↩︎

  2. 通过rel="preload"进行内容预加载 - HTML(超文本标记语言) | MDN ↩︎