原理部分

JeremyJone ... 2023-7-13 大约 8 分钟

# 原理部分

# 浏览器的渲染机制和原理

当我们在浏览器地址栏中键入一个地址,然后按下回车,此时一个请求便开始了。

这个过程分成两个阶段:

  • 请求阶段:负责从服务器获取内容
  • 渲染阶段:负责将获取到的内容呈现在浏览器中

# 请求阶段

1、当客户端开输入一个网址后,会向服务器发送一个 Request 请求,首先需要 DNS 解析,然后进行 TCP 连接。

2、服务器收到请求后,会发送一个 Response 响应给客户端,并将文件内容返回给客户端。

说明

实际请求要比上述过程更加繁琐,但并不影响对于渲染的理解。

# 渲染阶段

1、客户端拿到页面内容后,浏览器会在内存中开辟一块栈内存,用来给代码的执行提供环境,同时分配一个主线程一行一行解析和执行代码。

因为 JavaScript 是单线程,所以每执行完一条语句,都需要执行出栈操作,然后将下一条语句执行进栈操作

2、当浏览器遇到 linkscriptimgvideo 等资源请求,都会开辟一个全新的线程去加载资源文件,这个全新的线程叫 任务队列(Task Queue)

3、当主文件(不包含资源文件)第一次自上而下加载完成后,会生成 DOM-Tree

4、加载完 DOM-Tree 后,浏览器会去任务队列循环查看那些任务已经完成,然后将已完成的任务一个一个插入到 DOM-Tree 中,知道所有任务全部完成。这叫 事件循环(Event Loop)

任务队列又分成 微任务宏任务微任务 的优先级高于 宏任务

5、当 CSS 处理完成后,会生成 CSSOM,浏览器会将 DOM-TreeCSSOM 合并成一个 渲染树(Render Tree)

6、回流。浏览器根据生成的 Render Tree,计算它们在设备视口内的确切位置和大小,这个计算阶段叫做 回流(Reflow)

7、重绘。根据 Render Tree 以及 回流 得到的几何信息,得到节点的绝对像素,这个阶段叫做 重绘(Repaint)

在首次加载阶段,一定会发生 回流重绘,并且一定先 回流重绘

8、最后,浏览器会调用 GPU 进行图形渲染,将 Render Tree 的内容渲染并展示给用户。

# 页面加载时的阻塞

页面加载时,浏览器会逐行解析 html 内容,这是由 GUI 渲染线程所控制的。在此过程中,GUI 渲染线程会逐行解析,同时生成 DOM 树。

  • 当所有内容都解析完成,会触发 DOMContentLoaded 事件,此事件的触发无需等待样式表、图片、脚本等资源的加载。
  • 当一个页面包含所有资源被加载完成并解析成功后,执行 load 事件。这也是为什么我们之前写 js 总是需要 window.onload = function() {}

# 加载时遇到 js 代码

如果在加载页面时,遇到了 <script> 标签,GUI 渲染线程会暂停渲染,并将控制权移交给 JS 引擎。此时浏览器将执行 JS 代码,如果是内联代码,则直接执行。如果是外部文件,则等待下载后再执行(渲染阶段)。所有代码执行完毕之后将控制权再移交给 GUI 渲染线程,浏览器继续渲染 DOM。

为了减少 <script> 标签对页面渲染的影响,可以通过:

  • 将所有 <script> 标签写在页面底部
  • 使用 async 关键字来加载外部文件。此时浏览器不会阻塞,当下载完成它会自动执行文件内容
  • 使用 defer 关键字来加载外部文件。此时浏览器会在 DOMContentLoaded 事件触发之前执行该文件内容

# 加载时遇到 css 代码

加载页面时,遇到 <link> 标签与 <style> 标签同样会影响页面的渲染。但是它不会阻塞 DOM 树的构建,只影响页面的渲染。

比如:我们有:

<h1 class="title">Hello World</h1>
<style>
  .title {
    padding: 1rem;
  }

  .text {
    color: red;
  }
</style>
<div class="text">Hello World</div>
1
2
3
4
5
6
7
8
9
10
11

页面会首先渲染 <h1> 元素到页面上,然后读取到 <style> 标签从而开始构建 CSSOM 树,构建完成后重新渲染 <h1> 元素,并继续渲染 <div> 元素。

这样会导致:

  • <div> 标签在 CSSOM 构建完毕之前不会渲染,也就是阻塞在了标签这里。
  • 标签之前的内容被渲染了两次,因为样式的变化,可能出现 屏闪 现象。

所以正确的方式我们应该将 <link><style> 放在页面最开始的位置,通常放在 head 中。

# 回流 Reflow 与重绘 Repaint

  • 回流:当元素的宽高、大小或者位置等影响布局的属性发生了变化,会触发重弄更新布局,导致渲染树重新计算布局和渲染,这个过程叫做回流。

    如下等情况会发生回流:

    • 页面初始化(即首次渲染)
    • 添加或删除 DOM 元素
    • 元素位置发生变化(left、right、top、bottom 等)
    • 元素尺寸发生变化(size、width、height、margin、padding 等)
    • 内容发生变化(图片大小、文本大小、内容增减等)
    • 浏览器窗口发生变化
  • 重绘:当元素样式发生改变,但是宽高、大小、位置等影响布局的属性不发生变化时,浏览器会进行重绘。

    如:outline、visibility、color 等不影响布局的属性发生变化

注意

页面首次加载一定会回流 回流一定触发重绘,而重绘不一定回流

# 避免 DOM 的回流

避免 DOM 的回流,可以有效提高前端性能。可以通过如下几点来避免:

  • 放弃传统操作 DOM 的方式(原生 js、jQuery 等),而是采用基于 vue、react 等框架,用数据影响视图的模式(MVVM)

  • 读写分离,利用现代浏览器的渲染队列机制

    现代浏览器一般都会自动维护一个渲染队列,把所有会引起回流、重绘的操作放入队列,当操作一定数量或到达一定时间后,浏览器会自动刷新队列,批处理所有内容。

    但是有一些属性,会导致浏览器立即刷新渲染队列:

    • offsetTop/Left/Width/Height
    • clientTop/Left/Width/Height
    • scrollTop/Left/Width/Height
    • widthheight
    • getComputedStylecurrentStyle

    这些属性为了获取到最精确的数值,会立即刷新所有需要回流的内容。

  • 样式集中改变

    不要一个一个属性的去改变,最好是一起写完样式,统一改变,这样可以减少回流次数。

    ✔ div.style.cssText = "width:20px;height:20px";
    
    // 而不是
    ❌ div.style.width = "20px";
    ❌ div.style.height = "20px";
    
    1
    2
    3
    4
    5

    当然,现代浏览器已经在很大程度上帮我们做好了这样的规划,当读取到样式改变的内容时,不会第一时间去回流,而是尝试继续读取后面的内容,尽可能将所有改变样式都做完后再一次性的进行回流和重绘。但我们仍然需要养成良好的习惯。

  • 集中添加元素

    添加元素时,不要一个一个直接往 DOM 中添加,而是将所有内容添加到 fragment 中,最后添加一次就好。

    使用 document.createDocumentFragment() 创建临时容器,再把新元素添加到该容器中,最后将该容器添加到 DOM 中,引发一次回流:

    当然还可以使用文档字符串,拼接之后一次性添加到 HTML 中:

  • 让动画效果脱离文档流

    将具有动画效果的元素尽可能脱离文档流。通过 position:absolute / fixed 的方式,让元素在一个全新层级,这样就不会影响大部分的页面元素,减少回流的计算。

  • 使用 CSS3 的硬件加速

    CSS3 提供了 GPU 加速功能。使用 transformopacityfilters 等属性时会触发硬件加速,避免回流和重绘。

    div.style.transform = "translateX(200px)";
    // 效果等同于
    div.style.left = "200px";
    
    1
    2
    3

    同时也要注意,过多使用可能会导致大量占用内存,性能消耗严重,字体模糊等问题。这些是需要兼顾考虑的。

  • 牺牲平滑度换取速度

    有时我们可以通过牺牲掉平滑度来换取更快的速度。因为每次元素移动 1px 都会引发回流,所以我们可以加大移动间距,比如尝试 2px 甚至 3px

  • 避免 table 布局和使用 CSS 的 JavaScript 表达式

    页面中 table 布局层级太多,会导致多次计算才能确定元素的属性,从而导致大量回流。