我们来从渲染层面探讨浏览器优化
1. 浏览器渲染原理和关键渲染路径
探究浏览器渲染优化之前,了解浏览器的渲染流程是必要的,如下图所示:
浏览器的渲染过程主要包括以下几步:
- 构建DOM树(DOM tree):从上到下解析HTML文档生成DOM节点树(DOM tree;构建CSSOM(CSS Object Model)树:加载解析样式生成CSSOM树;
- 构建渲染树(render tree):根据DOM树和CSSOM树,生成渲染树(render tree);
- 渲染树:按顺序展示在屏幕上的一系列矩形,这些矩形带有字体,颜色和尺寸等视觉属性;
- 布局(layout):遍历渲染树开始布局,计算每个节点的位置大小信息;
- 绘制(painting):遍历渲染树绘制所有节点,为每一个节点适用对应的样式。
- Composite(多个层合成)
首先来看几个问题:
- DOM树的构建是文档加载完成开始的? 这是一个渐进的过程。为达到更好的用户体验,呈现引擎会力求尽快将内容显示在屏幕上。它不必等到整个 HTML 文档解析完毕之后,就会开始构建呈现树和设置布局。在不断接收和处理来自网络的其余内容的同时,呈现引擎会将部分内容解析并显示出来。
参考文档:taligarsiel.com/Projects/ho…
- 渲染树是在DOM树和CSS样式树构建完毕才开始构建的吗? 这三个过程在实际进行的时候又不是完全独立,而是会有交叉。会造成一边加载,一边解析,一边渲染的工作现象。
参考文档:www.jianshu.com/p/2d522fc2a…
- css的标签嵌套越多,越容易定位到元素 css的解析是自右至左逆向解析的,嵌套越多越增加浏览器的工作量,而不会越快。
原文链接:blog.csdn.net/riddle1981/…
再来看几个渲染阻塞的问题:
- 渲染阻塞
当浏览器遇到一个
script
标记时,DOM 构建将暂停,直至脚本完成执行
,然后继续构建DOM。每次去执行JavaScript脚本都会严重地阻塞DOM树的构建
。 如果JavaScript脚本还操作了CSSOM,而正好这个CSSOM还没有下载和构建,浏览器甚至会延迟脚本执行和构建DOM,直至完成其CSSOM的下载和构建
。
所以,script
标签的位置很重要。实际使用时,可以遵循下面两个原则:
- CSS 优先:引入顺序上,CSS 资源先于 JavaScript 资源。
- JS置后:我们通常把JS代码放到页面底部,且JavaScript 应尽量少影响 DOM 的构建。
当解析html的时候,会把新来的元素插入dom树里面,同时去查找css,然后把对应的样式规则应用到元素上,查找样式表是按照从右到左的顺序去匹配的。
例如: div p {font-size: 16px}
,会先寻找所有p标签并判断它的父标签是否为div之后才会决定要不要采用这个样式进行渲染)。
所以,我们平时写CSS时,尽量用id和class,千万不要过渡层叠。
- css 加载会造成阻塞吗
DOM 和 CSSOM 通常是并行构建的,所以 CSS 加载不会阻塞 DOM 的解析。
然而,由于 Render Tree 是依赖于 DOM Tree 和 CSSOM Tree 的,所以他必须等待到 CSSOM Tree 构建完成,也就是 CSS 资源加载完成(或者 CSS 资源加载失败)后,才能开始渲染。
因此,CSS 加载会阻塞 Dom 的渲染。
由于 JavaScript 是可操纵 DOM 和 css 样式 的,如果在修改这些元素属性同时渲染界面(即 JavaScript 线程和 UI 线程同时运行),那么渲染线程前后获得的元素数据就可能不一致了。
因此为了防止渲染出现不可预期的结果,浏览器设置 GUI 渲染线程与 JavaScript 引擎为互斥的关系。
因此,样式表会在后面的 js 执行前先加载执行完毕,所以css 会阻塞后面 js 的执行
- 为什么 JS 阻塞页面加载
由于 JavaScript 是可操纵 DOM 的,如果在修改这些元素属性同时渲染界面(即 JavaScript 线程和 UI 线程同时运行),那么渲染线程前后获得的元素数据就可能不一致了。
因此为了防止渲染出现不可预期的结果,浏览器设置 GUI 渲染线程与 JavaScript 引擎为互斥的关系。
当 JavaScript 引擎执行时 GUI 线程会被挂起,GUI 更新会被保存在一个队列中等到引擎线程空闲时立即被执行。
从上面我们可以推理出,由于 GUI 渲染线程与 JavaScript 执行线程是互斥的关系,
当浏览器在执行 JavaScript 程序的时候,GUI 渲染线程会被保存在一个队列中,直到 JS 程序执行完成,才会接着执行。
因此如果 JS 执行的时间过长,这样就会造成页面的渲染不连贯,导致页面渲染加载阻塞的感觉。
推荐文章
追本溯源 浏览器渲染机制
从输入url,到页面渲染都发生了什么
性能优化——关键路径渲染优化
2. 回流与重绘
2.1 新建一个dom的过程
- 获取DOM后分割为多个图层
- 对每个图层的节点计算样式结果(Recalculate style--样式重计算)
- 为每个节点生成图形和位置(Layout--回流和重布局)
- 将每个节点绘制填充到图层位图中(Paint Setup和Paint--重绘)
- 图层作为纹理上传至GPU
- 符合多个图层到页面上生成最终屏幕图像(Composite Layers--图层重组)
2.2 什么是回流、重绘
回流(重排):当render tree中的一部分(或全部)因为元素的规模尺寸,布局,隐藏等改变而需要重新构建。这就称为回流(reflow)。当页面布局和几何属性改变时就需要回流
重绘:当render tree中的一些元素需要更新属性,而这些属性只是影响元素的外观,风格,而不会影响布局的,比如background-color。则就叫称为重绘。
我们要注意的是:回流必将引起重绘,而重绘不一定会引起回流。
常见的触发回流的属性:
常见的触发重绘的属性:
这里推荐一个网站:csstriggers.com
在控制台观察回流
我们可以通过开发者工具查看load之后是否有layout来判断是否发生了回流
我们还可以在线实时观察,通过设置该选项,页面重绘时会变为绿色
2.3 回流的优化点
- 用translate替代top改变
- 用opacity替代visibility
- 不要一条一条地修改 DOM 的样式,预先定义好 class,然后修改 DOM的className
- 把 DOM 离线后修改,比如:先把 DOM 给 display:none (有一次 Reflow),然后你修改100次,然后再把它显示出来
- 不要把 DOM 结点的属性值放在一个循环里当成循环里的变量,如offsetHeight,读取会引发回流
- 不要使用 table 布局,可能很小的一个小改动会造成整个 table 的重新布局
- 动画实现的速度的选择
- gif图会频繁触发重绘,但是它在标签中,所以没有独立图层,我们可以将其独立出来图层,这样可以使重绘的范围变小
- 对于动画新建图层,但是图层过多也会影响性能,只将需要重绘的部分做图层,其它尽量减少图层,will-change: transform也可以新建一个图层
- will-change: transform可以新建一个图层,接下来将要干什么
- 启用 GPU 硬件加速
2.4 创建新图层的方法
- 3D或透视变换(perspective transform)CSS属性
- 使用加速视频解码的
<video>
节点,gif图 - 拥有3D(WebGL)上下文或加速的2D上下文的
<canvas>
节点 - 混合插件(如Flash)
- 对自己的opacity做CSS动画或使用一个动画webkit变换的元素
- 拥有加速CSS过滤器的元素
- 元素有一个包含复合层的后代节点(一个元素拥有一个子元素,该子元素在自己的层里)
- 元素有一个z-index较低且包含一个复合层的兄弟元素(换句话说就是该元素在复合层上面渲染)
3. 避免layout thrashing(布局抖动)
-
避免回流 这块我们上面已经讲过,这里不再赘述。
-
读写分离
我们用代码进行一些读写操作,如下图,其中右上角的红色标记代表该任务过长
我们对操作进行读写分离:
为什么要读写分离?
多次改变DOM元素的属性,并不会引发多次回流重绘,而是将这些合并为一次,因为浏览器中有缓冲机制。浏览器会维护一个队列,把所有引起回流和重绘的操作放入队列中,如果队列中的任务数量或者时间间隔达到一个阈值的,浏览器就会将队列清空,进行一次批处理,这样可以把多次回流和重绘变成一次。这属于浏览器自身的性能优化
但当你进行读操作时,浏览器会立刻清空队列。因为队列中可能会有影响到这些属性或方法返回值的操作,即使你希望获取的信息与队列中操作引发的改变无关,浏览器也会强行清空队列,确保你拿到的值是最精确的。
所以我们要对读写操作分离,以避免触发多次回流。
document.body.addEventListener('click', function() {
// Read
var h1 = element1.clientHeight;
// Write
requestAnimationFrame(function() {
element1.style.height = (h1 * 2) + 'px';
));
));
document.body.addEventListener('click', function() {
// Read
var h2 = element2.clientHeight;
// Write
requestAnimationFrame(function() {
element2.style.height = (h2 * 2) + 'px';
));
));
另外我们可以使用第三方库FastDom来解决页面抖动
4. 复合线程与图层
复合线程(compositor thread)与图层(layers)
复合线程做什么
- 将页面拆分图层进行绘制再进行复合
- 利用DevTools了解网页的图层拆分情况
- 哪些样式仅影响复合
5. requestIdleCallback,requestIdleCallback
requestIdleCallback与requestAnimationFrame区别,requestAnimationFrame是一帧开始,requestIdleCallback是一帧结束后的空闲时间
6. 防抖,节流
7分钟理解JS的节流、防抖及使用场景
防抖debounce 与 节流throttle
7. 长列表优化
长列表:react-window
- 加载大列表、大表单的每一行严重影响性能
- Lazy loading仍然会让DOM变得过大
- windowing只渲染可见的行,渲染和滚动的性能都会提升
一个简洁、有趣的无限下拉方案
「前端进阶」高性能渲染十万条数据(虚拟列表)
前端面经题记:长列表怎么优化?
参考文章:
从 8 道面试题看浏览器渲染过程与性能优化
(1.6w字)浏览器灵魂之问,请问你能接得住几个?
「浏览器工作原理」写给女友的秘籍-渲染流程篇(1.1W字)
从浏览器渲染原理谈性能优化(2017版)