浏览器的渲染机制
1.页面加载过程
在介绍浏览器渲染过程之前,想简明扼要的介绍下页面的加载过程,有助于更好的理解后续渲染过程,要点如下:
- 浏览器根据DNS服务器得到域名的IP地址
- 向这个IP的机器发送HTTP请求
- 服务器收到、处理并返回HTTP请求
- 浏览器得到返回内容
2.浏览器渲染过程
2.1.构建渲染树
当我们生成DOM树和CSSOM树以后,就需要将这两棵树组合为渲染树。
在这一过程中,不是简单的将两者合并就行了。渲染树只会包括需要显示的节点和这些节点的样式信息,如果某个节点是display:none的,那么就不会在渲染树中显示。
2.2.浏览器如果渲染过程中遇到JS文件怎么处理
渲染过程中,如果遇到<script>就停止渲染,执行JS代码。因为浏览器渲染和JS执行共用一个线程,而且这里必须是单线程操作,多线程会产生渲染DOM冲突。JavaScript的加载、解析与执行会阻塞DOM的构建,也就是说,在构建DOM时,HTML解析器若遇到了JavaScript,那么它会暂停构建DOM,将控制权移交给JavaScript引擎,等JavaScript引擎运行完毕,浏览器再从中断的地方恢复DOM构建。
也就是说,如果想首屏渲染得越快,就越不应该在首屏就加载JS文件,这也是都建议将script标签放在body标签底部的原因。当然在当下,并不是说script标签必须放在底部,因为你可以给script标签添加defer或者async属性.
2.3.JS文件不只是阻塞DOM的构建,它会导致CSSOM也阻塞DOM的构建
原本DOM和CSSOM的构建是互不影响的,井水不犯河水,但是一旦引入JavaScript,CSSOM也开始阻塞DOM的构建,只有CSSOM构建完毕后,DON再恢复DOM构建。这是为什么呢?原因如下:
因为JavaScript不只是可以改DOM,它还可以改样式,也就是它可以更改CSSOM。因为不完整的CSSOM是无法使用的,如果JavaScript想访问CSSOM并更改它,那么在执行JavaScript时,必须要能拿到完整的CSSOM。所以就导致了一个现象,如果浏览器尚未完成CSSOM的下载和构建,而我们却想在此时运行脚本,那么浏览器将延迟脚本执行和DOM构建,直至其完成CSSOM的下载和构建。也就是说,在这种情况下,浏览器会先下载和构建CSSOM,然后再执行JavaScript,最后再继续构建DOM。
3.布局与绘制
当浏览器生成渲染树以后,就会根据渲染树来进行布局(也可以叫做回流)。这一阶段浏览器要做的事情是要弄清楚各个节点在页面中的确切位置和大小。通常这一行为也被成为"自动重排"。
布局流程的输出是一个"盒模型",它会精确地捕获每个元素在视口内的确切位置和尺寸,所有相对测量值都将转换为屏幕上的绝对像素。
布局完成后,浏览器会立即发出"Paint Setup"和"Paint"事件,将渲染树转换成屏幕上的像素。
4.async和defer的作用
接下来我们对比下 defer 和 async 属性的区别:
其中蓝色线代表JavaScript加载;红色线代表JavaScript执行;绿色线代表 HTML 解析。
4.1.情况1 <scriptsrc="script.js"></script>
没有 defer 或 async,浏览器会立即加载并执行指定的脚本,也就是说不等待后续载入的文档元素,读到就加载并执行。
4.2.情况2 <scriptasyncsrc="script.js"></script>
(异步下载)
async 属性表示异步执行引入的 JavaScript,与 defer 的区别在于,如果已经加载好,就会开始执行——无论此刻是 HTML 解析阶段还是 DOMContentLoaded 触发之后。需要注意的是,这种方式加载的 JavaScript 依然会阻塞 load 事件。换句话说,async-script 可能在 DOMContentLoaded 触发之前或之后执行,但一定在 load 触发之前执行。
4.3.情况3 <scriptdefersrc="script.js"></script>
(延迟执行)
defer 属性表示延迟执行引入的 JavaScript,即这段 JavaScript 加载时 HTML 并未停止解析,这两个过程是并行的。整个 document 解析完毕且 defer-script 也加载完成之后(这两件事情的顺序无关),会执行所有由 defer-script 加载的 JavaScript 代码,然后触发 DOMContentLoaded 事件。
defer 与相比普通 script,有两点区别:载入 JavaScript 文件时不阻塞 HTML 的解析,执行阶段被放到 HTML 标签解析完成之后;在加载多个JS脚本的时候,async是无顺序的加载,而defer是有顺序的加载。
5.为什么操作DOM慢
把 DOM 和 JavaScript 各自想象成一个岛屿,它们之间用收费桥梁连接。——《高性能 JavaScript》
JS 是很快的,在 JS 中修改 DOM 对象也是很快的。在JS的世界里,一切是简单的、迅速的。但 DOM 操作并非 JS 一个人的独舞,而是两个模块之间的协作。
过“桥”要收费——这个开销本身就是不可忽略的。我们每操作一次 DOM(不管是为了修改还是仅仅为了访问其值),都要过一次“桥”。过“桥”的次数一多,就会产生比较明显的性能问题。因此“减少 DOM 操作”的建议,并非空穴来风。
6.回流和重绘
重绘:当我们对DOM的修改导致了样式的变化、却并未影响其集合属性(比如修改了颜色或背景色)时,浏览器不需重新计算元素的几何属性、直接为该元素绘制新的样式
回流:当我们对DOM的修改引发了DOM几何尺寸的变化(比如修改元素的宽、高或隐藏元素)时,浏览器需要重新计算元素的集合属性(其他元素的集合属性和位置也会受到影响),然后再将计算的结果绘制出来。这个过程就是回流(也叫重排)。
我们知道,当网页生成的时候,至少会渲染一次。在用户访问的过程中,还会不断重新渲染。重新渲染会重复回流+重绘或者只有重绘。回流必定会发生重绘,重绘不一定会引发回流。重绘和回流会在我们设置节点样式时频繁出现,同时也会很大程度上影响性能。回流所需的成本比重绘高的多,改变父节点里的子节点很可能会导致父节点的一系列回流。
6.1.常见引起回流属性和方法
任何会改变元素几何信息(元素的位置和尺寸大小)的操作,都会触发回流,
-
-
添加或者删除可见的DOM元素;
-
元素尺寸改变——边距、填充、边框、宽度和高度
-
内容变化,比如用户在input框中输入文字
-
浏览器窗口尺寸改变——resize事件发生时
-
计算 offsetWidth 和 offsetHeight 属性
-
设置 style 属性的值
-
6.2.常见引起重绘属性和方法
6.3.如何减少回流、重绘
-
-
使用 transform 替代 top
-
使用 visibility 替换 display: none ,因为前者只会引起重绘,后者会引发回流(改变了布局)
-
不要把节点的属性值放在一个循环里当成循环里的变量。
-
-
-
不要使用 table 布局,可能很小的一个小改动会造成整个 table 的重新布局
-
动画实现的速度的选择,动画速度越快,回流次数越多,也可以选择使用 requestAnimationFrame
-
CSS 选择符从右往左匹配查找,避免节点层级过多
-
将频繁重绘或者回流的节点设置为图层,图层能够阻止该节点的渲染行为影响别的节点。比如对于 video 标签来说,浏览器会自动将该节点变为图层。
-
7.性能优化策略
基于上面介绍的浏览器渲染原理,DOM 和 CSSOM 结构构建顺序,初始化可以对页面渲染做些优化,提升页面性能。
-
JS优化:
<script>
标签加上 defer属性 和 async属性 用于在不阻塞页面文档解析的前提下,控制脚本的下载和执行。defer属性:用于开启新的线程下载脚本文件,并使脚本在文档解析完成后执行。async属性:HTML5新增属性,用于异步下载脚本文件,下载完毕立即解释执行代码。 -
CSS优化:
<link>
标签的 rel属性 中的属性值设置为 preload 能够让你在你的HTML页面中可以指明哪些资源是在页面加载完成后即刻需要的,最优的配置加载顺序,提高渲染性能
8.总结
综上所述,我们得出这样的结论:
-
浏览器工作流程:构建DOM -> 构建CSSOM -> 构建渲染树 -> 布局 -> 绘制。
-
CSSOM会阻塞渲染,只有当CSSOM构建完毕后才会进入下一个阶段构建渲染树。
-
通常情况下DOM和CSSOM是并行构建的,但是当浏览器遇到一个不带defer或async属性的script标签时,DOM构建将暂停,如果此时又恰巧浏览器尚未完成CSSOM的下载和构建,由于JavaScript可以修改CSSOM,所以需要等CSSOM构建完毕后再执行JS,最后才重新DOM构建。
参考---https://mp.weixin.qq.com/s/sn7effRU3KVcJ8NYxLQefw