原理部分
# 原理部分
# 浏览器的渲染机制和原理
当我们在浏览器地址栏中键入一个地址,然后按下回车,此时一个请求便开始了。
这个过程分成两个阶段:
- 请求阶段:负责从服务器获取内容
- 渲染阶段:负责将获取到的内容呈现在浏览器中
# 请求阶段
1、当客户端开输入一个网址后,会向服务器发送一个 Request 请求,首先需要 DNS
解析,然后进行 TCP
连接。
2、服务器收到请求后,会发送一个 Response 响应给客户端,并将文件内容返回给客户端。
说明
实际请求要比上述过程更加繁琐,但并不影响对于渲染的理解。
# 渲染阶段
1、客户端拿到页面内容后,浏览器会在内存中开辟一块栈内存,用来给代码的执行提供环境,同时分配一个主线程一行一行解析和执行代码。
因为
JavaScript
是单线程,所以每执行完一条语句,都需要执行出栈操作,然后将下一条语句执行进栈操作
2、当浏览器遇到 link
、script
、img
、video
等资源请求,都会开辟一个全新的线程去加载资源文件,这个全新的线程叫 任务队列(Task Queue)。
3、当主文件(不包含资源文件)第一次自上而下加载完成后,会生成 DOM-Tree
。
4、加载完 DOM-Tree
后,浏览器会去任务队列循环查看那些任务已经完成,然后将已完成的任务一个一个插入到 DOM-Tree
中,知道所有任务全部完成。这叫 事件循环(Event Loop)。
任务队列又分成 微任务 和 宏任务,微任务 的优先级高于 宏任务
5、当 CSS 处理完成后,会生成 CSSOM
,浏览器会将 DOM-Tree
与 CSSOM
合并成一个 渲染树(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>
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
width
、height
getComputedStyle
、currentStyle
这些属性为了获取到最精确的数值,会立即刷新所有需要回流的内容。
样式集中改变
不要一个一个属性的去改变,最好是一起写完样式,统一改变,这样可以减少回流次数。
✔ 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 加速功能。使用
transform
、opacity
、filters
等属性时会触发硬件加速,避免回流和重绘。div.style.transform = "translateX(200px)"; // 效果等同于 div.style.left = "200px";
1
2
3同时也要注意,过多使用可能会导致大量占用内存,性能消耗严重,字体模糊等问题。这些是需要兼顾考虑的。
牺牲平滑度换取速度
有时我们可以通过牺牲掉平滑度来换取更快的速度。因为每次元素移动
1px
都会引发回流,所以我们可以加大移动间距,比如尝试2px
甚至3px
。避免 table 布局和使用 CSS 的 JavaScript 表达式
页面中 table 布局层级太多,会导致多次计算才能确定元素的属性,从而导致大量回流。