Skip to content
On this page

浏览器渲染原理

HTML 文件转换为 DOM 树

打开网页的时候,浏览器会请求对应的 HTML 文件。其实就是由字符串组成的一个文件,但是计算机硬件是不理解这些字符串的,所以在网络传输的内容其实是 0 和 1 这些字节数据。当浏览器介绍到这些字节数据后,它将这些字节数据转换成字符串,也就是我们写的代码。

bash
字节数据 => 字符串(代码)

当数据转换成字符串后,浏览器会将这些字符串通过词法分析转换为标记(token),这一过程在词法分析中叫标记化(tokennization)

bash
字节数据 => 字符串 => Token

标记还是字符串,是构成代码的最小单位。这一过程将代码分拆为一块块,并给这些内容打上标记,便于理解这些最小单位的代码是什么意思。

标记

当结束标记化后,这些标记会紧接着转换为 Node,最后这些 Node 会根据不同 Node 之前的联系构建为一颗 DOM 树

DOM 树

整个转换过程

bash
字节数据 => 字符串 => Token => Node => DOM

CSS 文件转换为 CSSOM 树

转换 CSS 到 CSSOM 树的过程和 HTML 类似

bash
字节数据 => 字符串 => Token => Node => CSSOM

这一过程,浏览器确认每一个节点的样式到底是什么,并且这一过程其实是很消耗资源的。因为样式可以自行设置给某个节点,也可以通过继承获得。这一个过程,浏览器递归 CSSOM 树,然后确定元素到底是什么样式。

为什么会消耗资源?

html
<div>
  <a> <span></span> </a>
</div>
<style>
  span {
    color: red;
  }
  div > a > span {
    color: red;
  }
</style>

对于第一种设置样式的方式来说,浏览器只需要找到页面中所有的 span 标签然后设置颜色,但是对于第二种设置样式的方式来说,浏览器首先需要找到所有的 span 标签,然后找到 span 标签上的 a 标签,最后再去找到 div 标签,然后给符合这种条件的 span 标签设置颜色,这样的递归过程就很复杂。所以我们应该尽可能的避免写过于具体CSS 选择器,然后对于 HTML 来说也尽量少的添加无意义标签,保证层级扁平

生产渲染树

DOM 树 + CSSOM 树 => 渲染树。

这一过程,不是简单的两者合并。渲染树只会包括需要显示的节点和这些节点的样式信息,如果某个节点是 display: none 的,那么就不会在渲染树中显示。

当浏览器生成渲染树以后,就会根据渲染树来进行布局(也可以叫做回流),然后调用 GPU 绘制,合成图层,显示在屏幕上。

整个步骤:

  • 加载整体 HTML 文件

  • 至上而下解析 HTML

  • 解析 HTML 建立 DOM 树,遇到诸如<script><link>等标签时,就会去下载相应内容,并解析、执行。

  • 如果是 <link> 标签,解析 CSS 构建 CSSOM

  • DOMCSSOM 结合生成 render

  • 布局 render 树(Layout/reflow),负责各元素尺寸、位置的计算

  • 绘制 render 树(paint),绘制页面像素信息

  • 浏览器会将各层的信息发送给 GPUGPU 会将各层合成(composite),显示在屏幕上。

repaint-reflow-2.png

重排和重绘

当浏览器打开任何一个页面,在页面首次渲染时,后面会伴随着一系列操作,如 JavaScript 脚本动态操作 DOM 或 CSSOM,用户的输入,异步加载,动效,用户滚动页面,用户调整浏览器视窗大小等,都会在首次渲染的基础上进行更新。

所以,页面首次都会进行一次绘制。在这以后,对构建渲染树信息进行任何改变都会造成两种结果:

  • 渲染树的部分(全部)内容需要重新验证,并重新计算节点尺寸(布局有变化)。这个过程就是重排或者回流布局

  • 页面部分内容需要获取更新,如节点(Node)的几何信息变化,或者一些类似背景颜色之类的 CSS 样式上的变化(样式有变化)。这个过程就是 重绘

页面的初始布局至少会重排一次;重绘不一定导致重排,但重排一定会导致重绘。

  • 重排(reflow):DOM 布局发生改变,引起重排(改变窗口大小,字体大小...)

  • 重绘(repaint):元素的 CSS 属性发生改变,引起重绘(color,border-style,background...)

引起重排的操作有:

  1. 页面首次渲染。
  2. 浏览器窗口大小发生改变。
  3. 元素尺寸或位置发生改变。
  4. 元素内容变化(文字数量或图片大小等等)。
  5. 元素字体大小变化。
  6. 添加或者删除可见的 DOM 元素。
  7. 激活 CSS 伪类(例如::hover)。
  8. 设置 style 属性
  9. 查询某些属性或调用某些方法。

常见引起重排属性和方法

width, height, border ...

减少重绘和回流

  • 使用 transform 替代 top
html
<div class="test"></div>
<style>
  .test {
    position: absolute;
    top: 10px;
    width: 100px;
    height: 100px;
    background: red;
  }
</style>
<script>
  setTimeout(() => {
    // 引起回流
    document.querySelector('.test').style.top = '100px'
  }, 1000)
</script>
  • 使用 visibility 替换 display: none ,因为前者只会引起重绘,后者会引发回流(改变了布局)

  • 修改频繁的元素先 display: none,修改完之后显示

  • 不要逐条修改 DOM 样式,尽量提前设置好 class,后续增加 class,进行批量修改

  • 使用 documentFragment 对象在内存里操作 DOM

  • 不要把节点的属性值放在一个循环里当成循环里的变量

js
for(let i = 0; i < 1000; i++) {
    // 获取 offsetTop 会导致回流,因为需要去获取正确的值
    console.log(document.querySelector('.test').style.offsetTop)
}
  • 不要使用 table 布局,可能很小的一个小改动会造成整个 table 的重新布局

  • 动画实现的速度的选择,动画速度越快,回流次数越多,也可以选择使用 requestAnimationFrame

  • CSS 选择符从右往左匹配查找,避免节点层级过多

  • 将频繁重绘或者回流的节点设置为图层,图层能够阻止该节点的渲染行为影响别的节点。比如对于 video 标签来说,浏览器会自动将该节点变为图层。

设置节点为图层的方式有很多,我们可以通过以下几个常用属性可以生成新图层

  • will-change
  • video、iframe 标签

操作 DOM 慢

操作 DOM 性能很差,但是其中是什么原因?

因为 DOM 是属于渲染引擎中的东西,而 JS 又是 JS 引擎中的事物,当我们通过 JS 操作 DOM 的时候,其实这个操作涉及到了两个线程之间的通信,那么势必会带来性能上的损耗。操作 DOM 次数一多,也就等同于一直在进行线程之间的通信,并且操作 DOM 可能还会带来重绘回流的情况,所以也就导致了性能上的问题。

插入几万个 DOM,如何实现页面不卡顿?

  • 分批次部分渲染 DOM

  • requestAnimationFrame

  • 虚拟滚动(virtualized scroller):只渲染可视区域内的内容,非可见区域的那就完全不渲染了,当用户在滚动的时候就实时去替换渲染的内容

什么情况阻塞渲染

渲染的前提是生成渲染树;所以 HTML 和 CSS 肯定会阻塞渲染。

渲染越快,应降低渲染文件的大小,并且避免多层无意义嵌套优化选址器

当浏览器在解析到 script 标签的时,会暂停构建 DOM ,浏览器必须等脚本下载完,并执行结束,如果想首屏渲染越快,就越不应该在首屏加载 JS 文件,这也是建议将 script 标签放在 body 标签底部的原因。

在现代,并不必须将 script 标签放在底部,因为可以给标签添加 deferasync 属性

defer

表示 JS 文件会并行下载,但是会放到 HTML 解析完成后顺序执行

告诉浏览器不要等待 JS 脚本。相反,浏览器将继续处理 HTML,构建 DOM。脚本会“在后台”下载,然后等 DOM 构建完成后,脚本才会执行

如果多个 JS 文件都存在 defer 属性时:

html
<script defer src="a.js"></script>
<script defer src="b.js"></script>

它们会并行下载;但是,defer 除了告诉浏览器“不要阻塞页面”之外,还会保持 JS 执行的相对顺序,因此,即使 b.js 先加载完成,它人需要等到 a.js 执行结束才会被执行。

提示

defer 特性仅适用于外部脚本

如果 <script> 脚本没有 src,则会忽略 defer 特性。

async

表示 JS 文件下载不会阻塞渲染(解析应该是会阻塞渲染的)

JS 是完全独立的

  • 浏览器不会因 async 脚本而阻塞

  • 其他 JS 不会等待 async 脚本加载完成,同样, async 脚本也不会等待其他 JS

  • 同时具有 async 属性的 JS,谁先加载谁执行,不保证顺序加载

当我们将独立的第三方脚本集成到页面时,此时采用异步加载方式是非常棒的:计数器广告等,因为它们不依赖于我们的脚本,我们的脚本也不应该等待它们

提示

async 特性仅适用于外部脚本

如果 <script> 脚本没有 src,则会忽略 async 特性。

defer.png

在不考虑缓存和优化网络协议的前提下,考虑可以通过哪些方式来最快的渲染页面

当发生 DOMContentLoaded 事件后,就会生成渲染树,生成渲染树就可以进行渲染了,这一过程更大程度上和硬件有关系了。

如何加速:

  • 文件大小

  • script 标签使用

  • HTMLCSS 代码书写格式

  • 首屏是否需要的文件

一些建议:

  • 减少文件大小,尽量压缩

  • script 标签异步加载

  • CSS 不要写多层级选择器,HTML 减少无意义标签,扁平化,避免多层无意义嵌套

  • 首页内容尽量优化,减少不必要加载或延后加载

  • 对首页布局样式无影响的 JS 延后加载