一、浏览器渲染页面的过程
基本流程包括:解析html以构建dom树 -> 构建render树 -> 布局render树 -> 绘制render树
1.代码解析:浏览器向服务器请求到了 HTML 文档后便开始解析,产物是 DOM ,根据 CSS 生成 CSSOM(CSS对象模型)
2.对象合成:将 DOM 和 CSSOM 合并产生渲染树(render tree)
3.布局(layout):有了渲染树,知道了所有节点的样式,便根据这些节点以及样式计算它们在浏览器中确切的大小和位置
4.绘制(paint):即遍历render树,把节点绘制到浏览器上
简述:浏览器会把HTML解析成DOM,把CSS解析成CSSOM,DOM和CSSOM合并就产生了渲染树(Render Tree) 有了RenderTree,我们就知道了所有节点的样式,然后计算他们在页面上的大小和位置,最后把节点绘制到页面上。
以上四步并非严格按顺序执行,为了更好的用户体验,渲染引擎将会尽可能早的将内容呈现到屏幕上,并不会等到所有的html都解析完成之后再去构建和布局render树。它是解析完一部分内容就显示一部分内容,同时,可能还在通过网络下载其余内容。
渲染的过程中值得注意的地方:
- html 一边解析一边显示,css 的加载和解析不会阻塞 html 文档的解析
- css 必须完全解析完毕才能进入生成渲染树环节,css的解析会阻塞js的执行,必须等到CSSOM生成后才能执行 js
- js 的执行会阻塞 html 文档的解析(加载外部脚本时,浏览器会暂停页面渲染,等待脚本下载并执行完成后,再继续渲染。原因是JavaScript可以修改DOM,所以必须把控制权让给它)
(图片来源于网络,侵删)
二、渲染过程的概念
重绘(repaint)
元素的视觉表现属性被改变即触发重绘,如visibility、opacity ,不会影响到 dom 结构。改变某个元素的背景色、文字颜色、边框颜色等等不影响它周围或内部布局的属性时,屏幕的一部分要重画,但是元素的几何尺寸没有变。
重排(reflow)/回流
就是渲染树的一部分必须要更新,并且节点的尺寸发生了变化。重排也会触发重绘。常见情况:
- DOM 操作,添加或者删除可见的DOM元素等
- 元素位置改变
- 修改 width、display 等 CSS 属性
- 内容改变——比如文本改变或者图片大小改变而引起的计算值宽度和高度改变
- 浏览器窗口变化(滚动或缩放)
- 页面渲染初始化
注意:重排(回流)必将引起重绘,而重绘不一定会引起回流。 display:none会触发reflow,而visibility:hidden只会触发repaint,因为没有发现位置变化。
三、页面优化
要进行页面优化,首要目标就是减少重绘和重排
减少重排、重绘其实就是需要减少对render tree的操作(合并多次多DOM和样式的修改)
1、CSS 标签应该放到头部(之间):如果 CSS 放在后面页面可能会出现闪跳、白屏或者布局混乱直到 CSS 加载完成。
2、script 标签都放在页面底部(前):这样即使遇到脚本失去响应,网页主体的渲染也已经完成了。还有一个好处,在DOM结构生成之前就调用DOM,JavaScript会报错,如果脚本都在网页尾部加载,就不存在这个问题,因为这时DOM肯定已经生成了。或者在标签中使用 async或defer特性 。
3、合理高效利用 CSS 选择器:CSS 是逆向解析的所以应当避免多层嵌套,避免使用通配器。尽量少的去对标签进行选择,而是用 class。不要去用标签限定ID或者类选择符,尽量少的去使用后代选择器,降低选择器的权重值。考虑继承。
4、 浏览器优化 现代浏览器大多都是通过队列机制来批量更新布局,浏览器会把修改操作放在队列中,至少一个浏览器刷新(即16.6ms)才会清空队列,但当你获取布局信息的时候,队列中可能有会影响这些属性或方法返回值的操作,即使没有,浏览器也会强制清空队列,触发回流与重绘来确保返回正确的值。
主要包括以下属性或方法: offsetTop、offsetLeft、offsetWidth、offsetHeight scrollTop、scrollLeft、scrollWidth、scrollHeight clientTop、clientLeft、clientWidth、clientHeight getComputedStyle() getBoundingClientRect
所以,我们应该避免频繁的使用上述的属性,他们都会强制渲染刷新队列。
5、批量修改DOM或者样式
最核心的思想就是对于对引起回流和重绘的操作不要一次一次的修改,应该用一个容器装起来,一次性修改
5.1、批量修改样式,比如修改样式如下:
const el = document.getElementById('test');
el.style.padding = '5px';
el.style.borderLeft = '1px';
el.style.borderRight = '2px';
//可以优化为使用cssText或者class统一添加
const el = document.getElementById('test');
el.style.cssText += 'border-left: 1px; border-right: 2px; padding: 5px;';
//或者
const el = document.getElementById('test');
el.className += ' active';
复制代码
5.2、减少对DOM的操作,将DOM节点保存在一个变量中,避免重复获取。通过使用DocumentFragment创建一个dom碎片,在它上面批量操作dom,操作完成之后,再添加到文档中,这样只会触发一次重排。
function appendDataToElement(appendToElement, data) {
let li;
for (let i = 0; i < data.length; i++) {
li = document.createElement('li');
li.textContent = 'text';
appendToElement.appendChild(li);
}
}
const ul = document.getElementById('list');
appendDataToElement(ul, data);
复制代码
如上面这种写法会导致多次回流 优化方式可以用document.createDocumentFragment
const ul = document.getElementById('list');
const fragment = document.createDocumentFragment();
appendDataToElement(fragment, data);
ul.appendChild(fragment);
复制代码
5.3、前面有说到渲染树回流、重绘都是针对可见元素,那么就有以下优化方案 原理就是利用display:none隐藏元素,进行各种增删改元素的操作,操作完再使其可见,对display:none隐藏元素进行操作是不会引起回流的。下面代码只会在ul显示的时候(display:block的时候)进行一次回流。
function appendDataToElement(appendToElement, data) {
let li;
for (let i = 0; i < data.length; i++) {
li = document.createElement('li');
li.textContent = 'text';
appendToElement.appendChild(li);
}
}
const ul = document.getElementById('list');
ul.style.display = 'none';
appendDataToElement(ul, data);
ul.style.display = 'block';
复制代码
5.4、避免触发同步UI渲染 在前面浏览器优化那一章中,提到那些关于获取节点(元素)布局信息的属性不能过分使用,会引起浏览器的强制回流或重绘,也就是在同步中进行回流重绘,所以我们也需要减少对这类属性的使用。
例子如下
function initP() {
for (let i = 0; i < paragraphs.length; i++) {
paragraphs[i].style.width = box.offsetWidth + 'px';
}
}
复制代码
这段代码看上去是没有什么问题,可是其实会造成很大的性能问题。在每次循环的时候,都读取了box的一个offsetWidth属性值,然后利用它来更新p标签的width属性。这就导致了每一次循环的时候,浏览器都必须先使上一次循环中的样式更新操作生效,才能响应本次循环的样式读取操作。每一次循环都会强制浏览器刷新队列。我们可以优化为:
const width = box.offsetWidth;
function initP() {
for (let i = 0; i < paragraphs.length; i++) {
paragraphs[i].style.width = width + 'px';
}
}
复制代码
5.5、对于复杂动画效果,使用绝对定位让其脱离文档流 对于复杂动画效果,由于会经常的引起回流重绘,因此,我们可以使用绝对定位,让它脱离文档流。否则会引起父元素以及后续元素频繁的回流 可以看这个例子,可以打开调试面板查看FPS的变化
例子中在未优化前,元素未绝对定位,依靠margin的变化进行动画,引起大量的回流,而当使用了绝对定位,明显感觉FPS稳定在每秒60次,也就是16.6ms一次。
6、css3硬件加速(GPU加速) 比起考虑如何减少回流重绘,我们更期望的是,根本不要回流重绘。这个时候,css3硬件加速就闪亮登场啦!! 划重点:
- 使用css3硬件加速,可以让transform、opacity、filters这些动画不会引起回流重绘 。
- 对于动画的其它属性,比如background-color这些,还是会引起回流重绘的,不过它还是可以提升这些动画的性能
如何使用
常见的触发硬件加速的css属性:transform
opacity
filters
Will-change
7、慎用hover:如果有大量元素使用:hover,那么会降低相应速度,CPU升高