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 树

  • DOM 和 CSSOM 结合生成 render 树

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

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

  • 浏览器会将各层的信息发送给 GPU,GPU 会将各层合成(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 标签放在底部,因为可以给标签添加 defer 或 async 属性

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 标签使用

  • HTML、CSS 代码书写格式

  • 首屏是否需要的文件

一些建议:

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

  • script 标签异步加载

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

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

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