浏览器的渲染过程(重点)
浏览器渲染主要有以下步骤:
-
首先解析收到的文档,根据文档定义构建 DOM 树。
DOM 树是由 DOM 元素及属性节点组成的。
-
然后对 CSS 进行解析,生成 CSSOM 规则树。
-
根据 DOM 树和 CSSOM 规则树构建渲染树。
渲染树的节点被称为渲染对象,渲染对象是一个包含有颜色和大小等属性的矩形,渲染对象和 DOM 元素相对应,但这种对应关系不是一对一的。
不可见的 DOM 元素不会被插入渲染树;还有一些 DOM 元素对应几个可渲染对象,它们一般是一些具有复杂结构的元素,无法用一个矩形来描述。
-
当浏览器生成渲染树以后,就会根据渲染树来进行布局(也可以叫做回流)。
这一阶段浏览器要做的事情是要弄清楚各个节点在页面中的确切位置和大小。
通常这一行为也被称为“自动重排”。
-
布局阶段结束后是绘制阶段,遍历渲染树并调用渲染对象的 paint 方法将它们的内容显示在屏幕上,绘制使用 UI 基础组件。
注意:以上这个过程是逐步完成的,为了更好的用户体验,渲染引擎将会尽可能早的将内容呈现到屏幕上,并不会等到所有的 HTML 都解析完成之后再去构建和布局渲染树。它是解析完一部分内容就显示一部分内容,同时,可能还在通过网络下载其余内容。
浏览器渲染优化(重点)
针对 CSS 的优化
使用 CSS 有三种方式:使用 link、@import、内联样式,其中 link 和 @import 都是导入外部样式。它们之间的区别:
- link:浏览器会派发一个新的线程(HTTP 线程)去加载资源文件,与此同时 GUI 渲染线程会继续向下渲染代码。
- @import:浏览器会等待 @import 引用的 CSS 文件下载并解析完成后,才继续构建 CSSOM。同时,由于 @import 必须出现在 CSS 文件中,这样会导致样式文件的串行下载(无法与其它外部资源并行加载),从而显著延长关键路径,阻碍页面渲染。
- style:GUI 直接渲染。
外部样式如果长时间没有加载完毕,浏览器为了用户体验,会使用浏览器会默认样式,以确保首次渲染的速度。所以 CSS 一般写在 head 中,让浏览器尽快发送请求去获取 CSS 样式。
所以,在开发过程中,导入外部样式使用 link,而不用 @import。如果 CSS 少,尽可能采用内嵌样式,直接写在 style 标签中。
针对 JS 的优化
JS 会阻塞 HTML 和 CSS 的解析,因此我们可以对 JS 的加载方式进行改变来进行优化。
-
尽量将 JS 文件放在 body 的最后。
-
body 中间尽量不要写
<script>标签。 -
<script>标签的引入资源方式有三种,有一种就是我们常用的直接引入,还有两种就是使用 async 属性和 defer 属性异步加载外部的 JS 文件,下载过程不会阻塞 DOM 解析。三者的区别如下:- script:立即停止页面渲染,下载并执行脚本,执行完毕后再继续渲染页面。下载和执行都会阻塞 DOM 解析。
- async:异步下载脚本,下载完成后立即执行,执行时会阻塞 DOM 解析。多个 async 脚本的执行顺序不保证,取决于哪个先下载完成。
- defer:异步下载脚本,但执行时机是在 DOM 解析完成后、DOMContentLoaded 事件触发前,不会阻塞 DOM 解析,并且按照它们在文档中出现的顺序依次执行。
针对 DOM 树、CSSOM 树的优化
可以通过以下几种方式来减少渲染的时间:
-
HTML 文件的代码层级尽量不要太深。
-
使用语义化的标签,来避免不标准语义化的特殊处理。
-
减少 CSS 代码的层级,因为选择器是从右向左进行解析的。
减少回流与重绘
-
操作 DOM 时,尽量在低层级的 DOM 节点进行操作。
-
不要使用 table 布局。 一个小的改动可能会使整个 table 进行重新布局。
-
使用 transform 代替 top/left 等触发重排的属性进行动画。
-
不要频繁操作元素的样式。 对于静态页面,可以修改类名,而不是样式。
-
**使用 absolute 或者 fixed,使元素脱离文档流。 **这样它们发生变化就不会影响其他元素。
-
避免频繁操作 DOM。 可以创建一个文档片段,在它上面应用所有 DOM 操作,最后再把它添加到文档中。
-
将元素先设置 display: none,操作结束后再把它显示出来。 因为在 display 属性为 none 的元素上进行的 DOM 操作不会引发回流和重绘。
-
将 DOM 的多个读操作(或者写操作)放在一起,而不是读操作穿插着写。
这得益于浏览器的渲染队列机制,具体如下:
浏览器针对页面的回流与重绘,进行了自身的优化——渲染队列。浏览器会将所有的回流、重绘的操作放在一个队列中,当队列中的操作到了一定的数量或者到了一定的时间间隔,浏览器就会对队列进行批处理。这样就会让多次的回流、重绘变成一次回流重绘。
将多个读操作(或者写操作)放在一起,就会等所有的读操作进入队列之后执行,这样,原本应该是触发多次回流,变成了只触发一次回流。
-
使用 CSS Containment。
通过 contain 属性(如 contain: layout paint size)告诉浏览器元素的变化不会影响其他元素,从而减少回流范围。
-
合理使用 will-change。
提前告知浏览器元素将要发生的变化,让浏览器有时间进行优化准备,但避免过度使用,否则可能占用过多资源。
渲染过程中遇到 JS 文件如何处理?(重点)
JS 的加载、解析与执行,默认情况下会阻塞文档的解析。
也就是说,在构建 DOM 时,HTML 解析器若遇到了 JS,那么它会暂停文档的解析,将控制权移交给 JS 引擎,等 JS 引擎运行完毕,浏览器再从中断的地方恢复继续解析文档。
也就是说,如果想要首屏渲染的越快,就越不应该在首屏就加载 JS 文件,这也是都建议将 script 标签放在 body 标签底部的原因。
当然并不是说 script 标签必须放在底部,因为你可以给 script 标签添加 defer 或者 async 属性。
什么情况会阻塞渲染?(重点)
首先,渲染的前提是生成渲染树,所以 HTML 和 CSS 肯定会阻塞渲染。
如果想渲染的越快,越应该降低一开始需要渲染的文件大小,并且扁平层级,优化选择器。
然后,当浏览器在解析到 script 标签时,会暂停构建 DOM,完成后才会从暂停的地方重新开始。
也就是说,如果想首屏渲染的越快,就越不应该在首屏就加载 JS 文件,这也是都建议将 script 标签放在 body 标签底部的原因。
当然并不是说 script 标签必须放在底部,因为你可以给 script 标签添加 defer 或者 async 属性。
当 script 标签加上 defer 属性以后,表示该 JS 文件会并行下载,但是会放到 HTML 解析完成后顺序执行(DOM 解析完成后、DOMContentLoaded 事件前执行),所以对于这种情况,可以把 script 标签放在任意位置。适合 JS 文件有依赖或需要操作 DOM 的情况。大多数脚本应该用 defer。
对于没有任何依赖且不操作 DOM 的 JS 文件可以加上 async 属性,表示 JS 文件下载不会阻塞页面渲染,解析时可能阻塞渲染。对于某些脚本(如广告、分析、监控),我们希望尽早执行,就可以使用 async,这种情况可以接受执行时可能出现的短暂阻塞渲染。
什么是文档的预解析?
Webkit 和 Firefox 都做了这个优化,当执行 JS 脚本时,另一个线程解析剩下的文档,并加载后面需要通过网络加载的资源。
这种方式可以使资源并行加载从而使整体速度更快。
需要注意的是,预解析并不改变 DOM 树,它将这个工作留给主解析过程,自己只解析外部资源的引用,比如外部脚本、样式表及图片。
CSS 如何阻塞文档解析?
理论上,既然样式表不改变 DOM 树,也就没有必要停下文档的解析等待它们。然而,存在一个问题,JS 脚本执行时可能在文档的解析过程中请求样式信息,如果样式还没有加载和解析,脚本将得到错误的值,显然这将会导致很多问题。所以如果浏览器尚未完成 CSSOM 的下载和构建,而我们却想在此时运行脚本,那么浏览器将延迟 JS 脚本执行和文档的解析,直至其完成 CSSOM 的下载和构建。也就是说,在这种情况下,浏览器会先下载和构建 CSSOM,然后再执行 JS,最后再继续文档的解析。
现代浏览器优化:实际上,现代浏览器会使用预加载扫描器(Preload Scanner)在解析 HTML 时提前发现并下载 CSS 和 JS 资源,从而减少阻塞时间。
如何优化关键渲染路径?
为尽快完成首次渲染,我们需要最大限度减小以下三种可变因素
-
关键资源的数量
关键资源是可能阻止网页首次渲染的资源。这些资源越少,浏览器的工作量就越小,对 CPU 以及其他资源的占用也就越少。
-
关键路径长度
关键路径长度受所有关键资源与其字节大小之间依赖关系图的影响,某些资源只能在上一资源处理完毕之后才能开始下载,并且资源越大,下载所需的往返次数就越多。
-
关键字节的数量
浏览器需要下载的关键字节越少,处理内容并让其出现在屏幕上的速度就越快。要减少字节数,我们可以减少资源数(将它们删除或设为非关键资源),此外还要压缩和优化各项资源,确保最大限度减少传送大小。
优化关键渲染路径的常规步骤
- 对关键路径进行分析和特性描述:资源数、字节数、长度
- 使用 Chrome DevTools 的 Performance 面板和 Lighthouse 工具进行分析
- 最大限度减少关键资源的数量:删除它们,延迟它们的下载,将它们标记为异步等
- 移除未使用的 CSS 和 JS
- 使用 async/defer 属性异步加载非关键脚本
- 延迟加载非关键资源(如图片、视频)
- 优化关键字节数以缩短下载时间(往返次数)
- 压缩 CSS 和 JS 文件
- 使用 WebP/AVIF 等现代图片格式
- 启用 HTTP/2 或 HTTP/3 协议
- 优化其余关键资源的加载顺序:您需要尽早下载所有关键资产,以缩短关键路径长度
- 将 CSS 放在 head 中,JS 放在 body 底部
- 使用 preload 和 prefetch 优化资源加载顺序
总结
本文详细介绍了浏览器的渲染原理及优化策略,包括:
- 浏览器渲染的五个核心步骤:构建 DOM 树、生成 CSSOM 规则树、构建渲染树、布局(回流)和绘制
- 针对 CSS、JS、DOM 树和 CSSOM 树的优化方法
- 减少回流与重绘的实用技巧
- 渲染过程中遇到 JS 文件的处理方式
- 阻塞渲染的常见情况及解决方案
- 文档预解析和 CSS 阻塞文档解析的原理
- 优化关键渲染路径的具体步骤
通过理解和应用这些浏览器渲染原理和优化策略,可以显著提升网页的加载速度和用户体验,特别是在首屏渲染和性能优化方面。