3. 浏览器渲染机制与优化
3.1 资源的请求和解析
请求是指资源获取,解析指的是代码的执行
- 客户端从服务器获取页面源代码
- 浏览器拿到源码之后开启 GUI 渲染进程,自上而下解析代码,最后绘制
- GUI自上而丰渲染解析代码过程整体是同步,但也会遇到异步行为
- CSS资源加载
- style 内嵌样式,GUI线程执行同步渲染行为
- link 外链样式,异步操作
- 开启 HTTP 网络请求线程
- 异步操作不会等到资源完全获取,GUI 线程会继续向下渲染
- GUI 线程将同步操作执行完成之后再处理基于 HTTP 网络请求回来的资源
- @import 导入样式(同步操作)
- 和 link 相同点是它也会开启 HTTP请求去下载资源
- 和 link 不同点是它会阻塞 GUI 线程渲染,在资源回来之前渲染不会继续
- Javascript 资源
- 默认情况下 script 都是同步操作
- 基于http请求,将目标资源请求回本地,由 JS 引擎处理完成,而且 GUI 再继续向下渲染
- script 标签默认会阻塞 GUI 的渲染
- window.onload 触发条件,所有资源都加载完成(包含 DOM TREE/CSS/图片等资源)
- DOMContentLoaded, DOM TREE加载完即可
- async 属性
- 遇到
<script async></script>
首先会开启 HTTP 请求加载 JS 资源 - GUI 渲染线程会继续向下渲染,将默认改为了异步
- 一旦资源请求回来就会中断 GUI 的渲染,先把请求回来的 JS 进行渲染解析(不记录顺序)
- 遇到
- defer 属性
- 遇到
<script defer></script>
和 async 一样,开启 HTTP 网络请求加载资源 - 此时 GUI 还会继续渲染,执行异步操作
- defer 和 link 类似,是在 GUI 同步代码渲染之后才会执行取回的 JS 资源
- 遇到
- 多媒体资源(img video)
- 异步操作,遇到请求之后开启 HTTP 线程去加载资源
- 异步操作不会阻塞 GUI 的渲染 (老版本浏览器会阻塞GUI渲染)
- 当 GUI 渲染完成之后才会处理请求回来的资源(link defer)
- 预测解析
- webkit 浏览器预测解析,chrome 的预加载扫描器 html-preload-scanner 通过扫描节点中的 src link 等属性,找到外部链接资源后进进预加载
- 上述操作可以避免资源加载的等待时间,同样实现了提前加载以及加载和执行分离
- 默认情况下 script 都是同步操作
3.2 页面渲染步骤
- DOM TREE(DOM树):自下而下渲染页面,整理好整个页面的 DOM 结构关系
- CSS TREE(样式树):当把所有的样式资源请求回来之后,按照引入 CSS 的顺序,依次渲染成样式代码,生成样式树
- RENDER TREE(渲染树):把生成的DOM树和CSS树结合在一起,生成渲染树(默认隐藏的元素不在渲染树中)
- Layout布局、回流、重排:依据生成的渲染树,计算元素们在设备视口(viewport)内确切位置和大小,这个过程是回流
- 分层处理:按照层级定位分层处理,每个层级都会有自己的绘制步骤
- Painting 绘制(重绘):将每层计算好的绘制步骤,开始绘制页面,依据渲染树及回流得到的几何信息,获取节点绝对像素,最后执行绘制
3.3 CRP 关键节点及优化
- 不使用 @import 导入样式
- link 标签放置于 head 标签 (提前加载样式资源,利用渲染树快速生成)
- 样式代码较少时直接使用内嵌,减少 HTTP 请求
- 减少 DOM或者减少 DOM 层级嵌套,标签语义化(开发时只把首屏结构写出来,只渲染首屏,等待首页渲染完成之后页面滚动时再基于 JS 创建其它屏的结构和内容,骨加屏/SSR)
- 把 script 放到页面的底部(先渲染DOM树,再执行JS)
- 图片合并、Base64、图片懒加载
3.4 回流和重绘
3.4.1 名词
- 回流:元素的大小或者位置发生了变化(页面布局和几何信息发生变化)触发重新布局,渲染树重新计算布局和渲染
- DOM 元素增删改查致结构发生改变
- DOM 样式(大小或者位置)发生改变
- DOM 内容发生改变
- 浏览器窗口大小发生改变(视口)
- 第一次加载必然会有一次回流
- 重绘:元素样式发生改变(但宽高、大小、位置信息不变)
- 回流一定会触发重绘,而重绘不一定会回流
3.4.2 优化方式
- 回流优化
- 使用文档碎片操作DOM
- 使用字符串拼接操作DOM
- 放弃传统DOM操作,基于 VUE/React
- 分离读写操作(设置和获取分开)
- 集中样式改变(设置样式操作集中写在一起)
- 缓存布局信息(将修改之前的样式保存在变量中)
- 元素批量修改
- 尽可能分层(但第一次渲染会慢),当前元素如果具备 position:absolute/fixed再或者具备 opacity/filters等属性,此时在修改样式或执行动画时会优化回流速度(只是优化回流,不是不回流)因为这些元素在重绘的时候只影响当前层
- 基于 transform 修改元素样式可以直接跳过渲染树,直接执行重绘,不回流
- 重绘
- 浏览器渲染队列机制, 上一行代码如果是修改元素的样式,此时并没有直接通知浏览器去渲染,
- 把它放置在浏览器渲染队列当中,继续向下执行代码,把执行遇到的修改样式操作全部放置到浏览器渲染队列中
- 如果不再有修改样式的操作,或者遇到了获取样式的操作则中断队列存放操作把队列中的渲染操作执行一次(引发一次回流)继续向下执行代码
3.4.3 渲染进程
const oBox = document.getElementById('root')
oBox.onclick = function () {
// 立即回到左侧
oBox.style.left = 0
oBox.style.top = 0
oBox.style.transitionDuration = '0s'
// oBox.offsetLeft
// 移动到别外
oBox.style.transitionDuration = '1s'
oBox.style.left = '400px'
}