JS进阶 - reflow & repaint 的性能优化
页面的渲染过程及优化渲染方式
浏览器内核
目前的 Chrome 浏览器为多进程多线程架构,包括 Browser进程、Renderer进程、GPU进程。
浏览器主进程:负责浏览器界面的显示、各个页面的管理。每次我们打开浏览器,都会启动一个主进程,结束该进程就会关闭我们的浏览器。主进程和渲染进程是分离的,这样可以保证页面的崩溃不会导致浏览器的崩溃。
渲染进程:这是网页的渲染进程,负责页面的渲染工作,一般来说,一个页面都会对应一个渲染进程,各自互相独立互不影响。
GPU 进程:如果页面启动了硬件加速,浏览器就会开启一个 GPU 进程,但是最多只能有一个,当且仅当 GPU 硬件加速打开的时候才会被创建。
每个进程都是多线程的,其目的是保持用户界面的高响应度,保证 UI 线程不会被其他操作阻碍从而影响了对用户操作的响应,例如 JS 脚本的解析执行都是在独立的线程中运行的。
页面渲染过程
页面从输入 URL 到页面渲染完成,主要经历了这几个阶段:
- DNS 解析
- 建立 TCP 连接
- 发送 HTTP 请求
- 渲染页面
- 断开连接
DNS 解析
DNS 解析的顺序依次为:
- 浏览器的 DNS 缓存,chrome 的缓存有效期为 60s,safari 有效期大概 10 多秒
- 系统缓存:hosts 文件设置的映射关系
- 路由器缓存
- ISP(互联网服务提供商)服务器缓存
- 根域名服务器缓存(保存着 顶级域名服务器 的ip地址)
- 顶级域名服务器缓存(例如 .com 域名服务器,保存着 主域名服务器 的ip地址)
- 主域名服务器缓存(例如 google.com 域名服务器)
渲染页面
浏览器的渲染过程如下图所示:
- 解析 HTML,生成 DOM 树,解析 CSS,生成 CSS 规则;
- 将 DOM 树和 CSS 规则结合,生成渲染树;
- 根据生成的渲染树进行重排(reflow),得到节点的几何信息;
- 根据重排获得的数据进行重绘(repaint);
- 将像素发送给 GPU 进行展示;
reflow 重排
重排指的是浏览器重新计算文档中元素的几何信息(位置和大小)的过程。当一个元素发生重排时,他的父元素和子元素都可能发生重排。
常见的会导致重排情况:
- 调整浏览器窗口大小
- 增加或移出样式表
- 内容发生变化
- 改变字体
- Js 操作 DOM
- 修改
style
属性的值 - 读取
offset
、client
、scroll
属性或调用getComputedStyle()
、getBoundingClientRect()
方法 - CSS 伪类激活,例如
:hover
repaint 重绘
当元素的样式更新时就会触发重绘,当发生 reflow 时,必定触发 repaint。
具体什么属性会触发 reflow 和 repaint 见 csstriggers
优化
避免触发同步布局事件:
浏览器为了防止多次重排操作引发假死,会通过队列化修改并批量执行来优化重排的过程,但如果调用 offsetTop
等属性时,浏览器为了获取到真实的值,会强制队列刷新并立即重排, 所以避免使用以下属性或方法:
offsetTop/Left/Widt/Height
scrollTop/Left/Width/Height
clientTop/Left/Width/Height
getComputedStyle()
getBoundingClientRect()
如果必须使用这些属性,可以将属性值缓存起来:
const width = box.offsetWidth;
function initP() {
for (let i = 0; i < paragraphs.length; i++) {
paragraphs[i].style.width = width + 'px';
}
}
合并DOM操作
优化重排和重绘最好的方式就是减少他们的发生次数,为此可以将多次DOM操作合并为1次:
const el = document.getElementById('test');
el.style.padding = '5px';
el.style.borderLeft = '1px';
el.style.borderRight = '2px'; // 3次分离的修改触发了3次重排
// 使用cssText或class来合并修改
const el = document.getElementById('test');
el.style.cssText += 'border-left: 1px; border-right: 2px; padding: 5px;';
// or
el.className += ' active';
离线DOM操作
如果DOM操作无法合并,可以先将DOM脱离文档流,修改完成后再带回文档中,离线DOM主要有三种方式:
使用 display:none(两次回流和重绘)
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';
使用 DocumentFragment 进行缓存操作(一次回流和重绘)
const ul = document.getElementById('list'); const fragment = document.createDocumentFragment(); appendDataToElement(fragment, data); ul.appendChild(fragment);
使用 cloneNode 和 replaceChild(一次回流和重绘)
const ul = document.getElementById('list'); const clone = ul.cloneNode(true); appendDataToElement(clone, data); ul.parentNode.replaceChild(clone, ul);
使用CSS硬件加速
使用css3硬件加速,transform、opacity、filters等动画不会引起回流重绘
JS进阶 - reflow & repaint 的性能优化