渲染机制
浏览器的渲染机制一般分为以下几个步骤
- 处理
HTML并构建DOM树。 - 处理
CSS构建CSSOM树。 - 将
DOM与CSSOM合并成一个渲染树。 - 根据渲染树来布局,计算每个节点的位置。
- 调用
GPU绘制,合成图层,显示在屏幕上

- 在构建 CSSOM 树时,会阻塞渲染,直至 CSSOM 树构建完成。并且构建 CSSOM 树是一个十分消耗性能的过程,所以应该尽量保证层级扁平,减少过度层叠,越是具体的 CSS 选择器,执行速度越慢。
- css 是阻塞渲染的资源。需要将它尽早、尽快地下载到客户端,以便缩短首次渲染的时间
- 当 HTML 解析到 script 标签时,会暂停构建 DOM,完成后才会从暂停的地方重新开始。也就是说,如果你想首屏渲染的越快,就越不应该在首屏就加载 JS 文件。并且 CSS 也会影响 JS 的执行,只有当解析完样式表才会执行 JS,所以也可以认为这种情况下,CSS 也会暂停构建 DOM
图层
一般来说,可以把普通文档流看成一个图层。特定的属性可以生成一个新的图层。不同的图层渲染互不影响,所以对于某些频繁需要渲染的建议单独生成一个新图层,提高性能。但也不能生成过多的图层,会引起反作用
- 通过以下几个常用属性可以生成新图层
3D变换:translate3d、translateZwill-changevideo、iframe标签- 通过动画实现的
opacity动画转换 position: fixed
重绘与回流
当元素的样式发生变化时,浏览器需要触发更新,重新绘制元素。这个过程中,有两种类型的操作,即重绘与回流。
- 重绘(repaint): 当元素样式的改变不影响布局时,浏览器将使用重绘对元素进行更新,此时由于只需要 UI 层面的重新像素绘制,因此 损耗较少
- 回流(reflow): 当元素的尺寸、结构或触发某些属性时,浏览器会重新渲染页面,称为回流。此时,浏览器需要重新经过计算,计算后还需要重新页面布局,因此是较重的操作。会触发回流的操作:
- 页面初次渲染
- 浏览器窗口大小改变
- 元素尺寸、位置、内容发生改变
- 元素字体大小变化
- 添加或者删除可见的
dom元素 - 激活
CSS伪类(例如::hover) - 查询某些属性或调用某些方法
clientWidth、clientHeight、clientTop、clientLeftoffsetWidth、offsetHeight、offsetTop、offsetLeftscrollWidth、scrollHeight、scrollTop、scrollLeftgetComputedStyle()getBoundingClientRect()scrollTo()
回流必定触发重绘,重绘不一定触发回流。重绘的开销较小,回流的代价较高。
回流的优化
对树的局部甚至全局重新生成是非常耗性能的,所以要避免频繁触发回流
- 现代浏览器已经帮我们做了优化,采用队列存储多次的回流操作,然后批量执行,但获取布局信息例外,因为要获取到实时的数值,浏览器就必须要清空队列,立即执行回流。
- 编码上,避免连续多次修改,可通过合并修改,一次触发
- 减少
dom的增删次数,可使用 字符串 或者documentFragment一次性插入 - 对于大量不同的
dom修改,可以先将其脱离文档流,比如使用绝对定位,或者display:none,在文档流外修改完成后再放回文档里中 - 将动画效果应用到
position属性为absolute或fixed的元素上 - 动画实现的速度的选择,动画速度越快,回流次数越多,也可以选择使用
requestAnimationFrame - 通过节流和防抖控制触发频率
css3硬件加速,transform、opacity、filters,开启后,会新建渲染层
另外,可以借助 DevTools Performance 面板来查看产生回流重绘任务占用主线程的情况和调用代码
开启 GPU 加速的方法
开启后,会将 dom 元素提升为独立的渲染层,它的变化不会再影响文档流中的布局。
transform: translateZ(0)opacityfiltersWill-change
很多人不知道的是,重绘和回流其实和 Event loop 有关
- 当
Event loop执行完Microtasks后,会判断document是否需要更新。因为浏览器是60Hz的刷新率,每16ms才会更新一次。 - 然后判断是否有
resize或者scroll,有的话会去触发事件,所以resize和scroll事件也是至少16ms才会触发一次,并且自带节流功能。 - 判断是否触发了
media query - 更新动画并且发送事件
- 判断是否有全屏操作事件
- 执行
requestAnimationFrame回调 - 执行
IntersectionObserver回调,该方法用于判断元素是否可见,可以用于懒加载上,但是兼容性不好 - 更新界面
- 以上就是一帧中可能会做的事情。如果在一帧中有空闲时间,就会去执行
requestIdleCallback回调
JavaScript 会阻塞 DOM 生成
JavaScript会阻塞DOM生成,而样式文件又会阻塞JavaScript的执行,所以在实际的工程中需要重点关注JavaScript文件和样式表文件,使用不当会影响到页面性能的
当渲染进程接收 HTML 文件字节流时,会先开启一个预解析线程,如果遇到 JavaScript 文件或者 CSS 文件,那么预解析线程会提前下载这些数据
- 如果代码里引用了外部的 CSS 文件,那么在执行 JavaScript 之前,还需要等待外部的 CSS 文件下载完成,并解析生成 CSSOM 对象之后,才能执行 JavaScript 脚本。
- 而 JavaScript 引擎在解析 JavaScript 之前,是不知道 JavaScript 是否操纵了 CSSOM 的,所以渲染引擎在遇到 JavaScript 脚本时,不管该脚本是否操纵了 CSSOM,都会执行 CSS 文件下载,解析操作,再执行 JavaScript 脚本。
- 不管 CSS 文件和 JavaScript 文件谁先到达,都要先等到 CSS 文件下载完成并生成 CSSOM,然后再执行 JavaScript 脚本,最后再继续构建 DOM,构建布局树,绘制页面
缩短白屏时长,可以有以下策略
- 通过内联 JavaScript、内联 CSS 来移除这两种类型的文件下载,这样获取到 HTML 文件之后就可以直接开始渲染流程了。
- 但并不是所有的场合都适合内联,那么还可以尽量减少文件大小,比如通过 webpack 等工具移除一些不必要的注释,并压缩 JavaScript 文件。
- 还可以将一些不需要在解析 HTML 阶段使用的 JavaScript 标记上
sync或者defer - 对于大的 CSS 文件,可以通过媒体查询属性,将其拆分为多个不同用途的 CSS 文件,这样只有在特定的场景下才会加载特定的 CSS 文件。
Cao Note