21

大家是不是会遇到这样的一个问题,页面加载速度过慢,浏览器老在转圈圈,页面部分内容需要花费较多的时间才能加载出来?

要明白上述问题,我们需要知道是什么在阻塞页面的渲染?

1、浏览器如何渲染?

1.1、渲染引擎介绍
要先说明:Firebox 的渲染引擎是 Geoko,chrome的渲染引擎是wekit。本文使用的是chrome浏览器

1.2、渲染的主要过程

简单介绍
浏览器解析 DOM 生成 DOM Tree, 结合CSS 生成的 CSS Tree, 最终组成 Render Tree,再渲染页面。因此 在此过程 css 不会阻塞 DOM 解析。

详细介绍

流程示意图
clipboard.png

几个概念:

DOM Tree:浏览器将HTML解析成树形的数据结构
CSS Rule Tree:浏览器将 CSS 解析成树形的数据结构
Render Tree:DOM 和 CSSOM 合并后生成 Render Tree
layout:有了Render Tree, 浏览器已经知道网页中有哪些节点、各个节点的CSS定义以及他们的从属关系,从而去计算出每个节点在屏幕中的位置
painting:按照算出来的规则,把内容画到屏幕上
reflow(回流):当浏览器发现某个部分发生了点变化影响了布局,需要倒回去重新渲染,这个过程称为 reflow。reflow 会从 <html> 这个 root frame 开始递归往下,依次计算所有的结点尺寸和位置。reflow 是无法避免的。目前界面上流行的一些效果,比如树状目录的折叠、展开(实质上是元素的显示与隐藏)等,都将引起浏览器的 reflow。鼠标滑过、点击等等... 只要这些行为引起了页面上某些元素的占位面积、定位方式、边距等属性的变化,都会引起它内部、周围甚至整个页面的重新渲染
repaint(重绘):改变某个元素的背景色、文字颜色、边框颜色等等不影响它周围或内部布局的属性时,屏幕的一部分要重画,但是元素的几何尺寸没有变。

注意:
(1)display:none 的节点不会被加入Render Tree, 而 visibility:hidden则会;所以,如果某个节点最开始是不显示的,设为display:none 是更优的。
(2)display:none 会触发reflow, 而 visibility:hidden 只会触发repaint,因为位置没有变化
(3)有些情况下,比如修改了元素的样式浏览器并不会立刻reflow 或 repaint 一次,而是会把这样的操作积攒一批,然后做一次 reflow,这叫 异步 reflow 或 增量异步 reflow。但是在有些情况下,比如resize 窗口,改变了页面默认的字体等。对于这些操作,浏览器会马上进行reflow。

webkit 的主要流程:

clipboard.png

Geoko 的主要流程:

clipboard.png

完整流程解析:
(1)浏览器将 HTML 解析成DOM 树,当前节点的所有子节点都构建好后才会去构建当前节点的下一个兄弟节点。
(2)将CSS 解析成 CSS Rule Tree
(3)根据DOM 树 和 CSSOM 构造 Render Tree。注意,display:none 不会被挂载到 Render Tree 上面
(4)计算出每个节点在屏幕中的位置
(5)绘制!
注意:渲染引擎将会尽可能早的将内容呈现到屏幕上,并不会等到所有的html都解析完成之后再去构建和布局render tree。它是解析完一部分内容就显示一部分内容,同时,可能还在通过网络下载其余内容

展示一下。display:none 与 visibility:hidden 在浏览器上的区别

display:none

clipboard.png

visibility:hidden

clipboard.png

2、阻塞渲染:CSS 与 javascript

讨论资源的阻塞时,我们要清楚,现代浏览器总是并行加载资源。例如,当HTML解析器(HTML Parser) 被脚本阻塞时,解析器虽然会停止构建DOM,但仍会识别该脚本后面的资源,并进行预加载

同时,由于下面两点:

  1. 默认情况下,CSS 被视为阻塞渲染的资源,这意味着浏览器不会渲染任何已处理的内容,直至CSSOM构建完毕
  2. javascript 不仅可以读取和修改DOM 属性,还可以读取和修改CSSOM 属性

存在阻塞的 CSS 资源时, 浏览器会延迟javascript 的执行和 Render Tree 构建。

另外

  1. 当浏览器遇到一个script标记时,DOM 构建将暂停,直至脚本完成执行。
  2. javascript 可以查询和修改 DOM 与 CSSOM
  3. CSSOM 构建时,javascript 执行将暂停,直至 CSSOM 就绪。

所以,script 标签的位置很重要。实际使用时,可以遵循下面2个原则

  1. CSS 优先:引入顺序上,CSS 资源优于javascript资源
  2. javascript 应尽量少影响 DOM 的构建

2.1、CSS 会阻塞渲染吗?

从浏览器的渲染原理来看,渲染引擎会将css 构建成 CSSOM Tree 然后再渲染页面。也就是说,CSS 会 阻塞 页面的渲染!但是,CSS 并不会阻塞 DOM 的解析。(因为需要具有 DOM 以及 CSSOM 才会构建渲染树

2.2、JS 会阻塞渲染吗?

看以下代码:

<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8" />
        <title></title>
        <style>
            div {
                width: 100px;
                height: 100px;
                background: lightgreen;
            }
        </style>
    </head>
    <body>
        <div>dsadddadas</div>
    </body>
</html>

下面这段代码会在浏览器上面显示一个绿色的盒子

clipboard.png

第2个例子,在头部引入JS

// new_file.js
let arr = []
for    (let i = 0; i < 10000000; i++) {
    arr.push(i);
    arr.splice(i%3, i%7 ,i %5);
}
let div= document.querySelector('div');
console.log(div);
<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8" />
        <title></title>
        <style>
            div {
                width: 100px;
                height: 100px;
                background: lightgreen;
            }
        </style>
        <script src="js/new_file.js"></script>
    </head>
    <body>
        <div>box1</div>
        <div style="visibility: hidden;">box2</div>
        <div>box3</div>

    </body>
</html>

浏览器会先转圈圈,最后在显示 盒子

也就是说,为什么大部分程序都会将 js 放在 底部,css 放在顶部 就是为了加速页面的渲染

3、我们如何改变阻塞现状?defer 与 async

3.1、defer 与 async 的介绍

defer:js的加载不会阻塞页面的渲染和资源的加载,defer 会按照原本js的顺序执行。
async:js 的加载不会阻塞页面的渲染和资源的加载,一旦加载到就会立刻执行。如果js前后有依赖性,最好不要用async。

3.2、defer 与 async的区别
3.2.1、相同点:

  • 加载文件时不阻塞页面渲染
<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8" />
        <title></title>
        <style>
            div {
                width: 100px;
                height: 100px;
                background: lightgreen;
            }
        </style>
        <script src="js/new_file.js" defer></script>
    </head>
    <body>
        
        <div>box1</div>
        <div>box3</div>

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

会在一运行的时候,就在页面中显示2个盒子,因此不阻塞

  • 对于inline的script(内联脚本)无效
<script>
    let arr = []
    for    (let i = 0; i < 10000000; i++) {
        arr.push(i);
        arr.splice(i%3, i%7 ,i %5);
    }    
    console.log("...")
</script>
<script async>
    console.log("async")
</script>
<script defer>
    console.log("defer")
</script>

clipboard.png

浏览器按顺序打印!

  • 使用这两个属性的脚本中不能调用document.write 方法
// new_file.js
let arr = []
for    (let i = 0; i < 10000000; i++) {
    arr.push(i);
    arr.splice(i%3, i%7 ,i %5);
}
let div= document.querySelector('div');
console.log(div);
document.write('asdadasas');

clipboard.png

  • 有脚本的onload 事件回调

3.2.2、不同点:

  • 每一个async 属性的脚本都在它下载结束后之后立刻执行,同时会在window的load事件之前执行。
  • 每一个defer属性的脚本都是在页面解析完毕之后,按照原本的属性执行,同时会在document的DOMContentLoader之前执行

参(抄)考(袭)文章:
https://juejin.im/post/59c606... 原来 CSS 与 JS 是这样阻塞 DOM 解析和渲染的

https://juejin.im/entry/59e1d... 浏览器的渲染:过程与原理

https://juejin.im/post/5a1229... script中defer和async的区别

https://www.cnblogs.com/Bonni... load/domContentLoaded事件、异步/延迟Js 与DOM解析


心无私天地宽
513 声望22 粉丝