JavaScript DOM的性能优化详解
本身JS操作DOM就比较消耗性能,你可以理解为JS和dom是独立的小岛,用桥实现两者的联系,但桥很窄,要过路费,所以我们要尽最大可能减少过桥的次数。
再加上每次操作DOM都会触发布局的改变、DOM树的修改和渲染。也就是说操作DOM会引发重排(回流)与重绘,这个过程也是非常消耗资源的。所以要尽量避免重复操作DOM元素。
DOM性能优化的本质:
就是减少对DOM查询及减少DOM操作(增、删、改)引起的重排(回流)和重绘的次数
DOM性能优化方法:
- 合并多次对css样式的修改,改为一次处理
- 对DOM查询做缓存
- 将频繁DOM操作改为一次性操作
- 操作DOM前,先把DOM节点删除或隐藏
- 采用事件代理处理事件
在讲解5种优化方法之前,我们需要先了解什么是重排(回流)和重绘,要了解重排(回流)和重绘,就需要先了浏览器的渲染机制,所以我们先从浏览器的渲染机制开始讲起。
一、浏览器的渲染机制
浏览器的整个渲染过程(下图)
- 解析 HTML,构建 DOM 树
- 解析 CSS,生成 CSS 规则树
- 合并 DOM 树和 CSS 规则树,生成 render(渲染)树。
- 布局 render 树(回流 / 重排),负责各元素尺寸、位置的计算。
- 绘制 render 树(painting 重绘),绘制页面像素信息
- 浏览器会将各层的信息发送给 GPU,GPU 会将各层合成(composite),显示在屏幕上。
构建渲染树时,浏览器主要完成以下工作
- 从 DOM 树的根节点开始遍历每个可见节点
- 对每个可见节点,找到 CSS 规则树中对应的规则,并应用它们
- 根据每个可见节点以及其对应的样式,组合生成渲染树
不可见节点(也就是不会出现在渲染树中的节点)
- 一些不会渲染输出的节点(如:script、meta、link 等)
- 一些通过 css 进行隐藏的节点(如:display: none)
注意点:
- 样式为;的节点会在DOM树中而不在渲染树中
- visiblity 和 opacity 隐藏的节点在DOM和渲染树中同时存在。
- 浏览器绘制之后便开始解析js文件,根据js对DOM的操作来确定是否会再次发生重排(回流)和重绘。
二、什么是重排(回流)和 重绘
重排(回流)
当渲染树(render tree)中的一部分或全部因为元素的规模尺寸、大小等改变时,浏览器需要重新计算元素在设备视口(viewport)内的确切位置和大小,需要重新布局render树,这个过程为回流(重排)。
重绘
当页面元素样式改变(如 color、background-color、visibility),但不影响元素在文档流中的的位置时,浏览器只需将新样式赋予元素并进行重新绘制render树操作,这个过程为重绘。
回流必将引起重绘,但重绘不一定会引起回流
什么情况会发生重排(回流)
- 添加或删除可见的 DOM 元素
- 元素的位置发生变化
- 元素的尺寸发生变化(外边距、内边距、边框大小、高度和宽度等)
- 内容发生变化,(比如文本变化或图片被尺寸大小发生变化)
- 页面渲染初始化(必然要首次重排)
- 浏览器的窗口 resize 尺寸变化(因为回流是根据视口的大小来计算元素的位置和大小的)
- 获取元素位置和大小相关的属性和方法,因为都需要返回最新的布局信息,因此浏览器不得不触发回流重绘来返回正确的值
- offset(Top / Left / Width / Height)
- scroll(Top / Left / Width / Height)
- client(Top / Left / Width / Height)
- width、height
- 调用了 getComputeStyle() 或者 IE 的 currentStyle
三、DOM性能优化的4种方法
① 合并多次对css样式的修改,改为一次处理
优化前样式代码
oLi.style.width = "100px";
oLi.style.height = "20px";
oLi.style.background = "pink";
方法一:用cssText一次性处理
oLi.style.cssText = "width:100px;height:20px;background:pink";
方法二:用className一次性处理
.item{width:100px;height:20px;background-color:pink;} /*定义类样式*/
oLi.className='item' //js添加类样式
三种情况下,消耗时间对比
方法一:优化前方法二:cssText处理后方法三:className处理后370.9970703125 ms243.667236328125 ms147.678955078125 ms
<body>
<ul id="list"></ul>
<script>
console.time("优化前"); //测试执行时间代码
var oUl = document.getElementById("list");
for (var i = 0; i < 50000; i++) {
var oLi = document.createElement("li");
//方法一
oLi.style.width = "100px";
oLi.style.height = "20px";
oLi.style.background = "pink";
//方法二:
// oLi.style.cssText = "width:100px;height:20px;background:pink";
//方法三:
/* oLi.className = "item";
oUl.appendChild(oLi);*/
}
console.timeEnd("优化前"); //测试执行时间代码
</script>
</body>
② 对DOM查询做缓存
不要频繁的去查询DOM,把查询到的内容保存在变量中,后面直接通过变量来操作就好。
未优化前,要频繁的查询DOM
for (var i = 0; i < document.querySelectorAll("li").length; i++) {....... }
优化后,只需要查询一次DOM
var n = document.querySelectorAll("li").length;
for (var i = 0; i < n; i++) {......}
优化后与优化前耗时对比未优化前耗时优化后耗时206.722900390625 ms154.436767578125 ms<script>
window.onload = function () {
//优化前代码
console.time("时间记录");//测试执行时间代码
for (var i = 0; i < document.querySelectorAll("li").length; i++) {
console.log(i);
}
/* 优化后代码
var n = document.querySelectorAll("li").length;
for (var i = 0; i < n; i++) {
console.log(i);
} */
console.timeEnd("时间记录"); //测试执行时间代码
};
</script>
<body>
<ul>
<li></li>
<!--以下重复,菜5000个li-->
</ul>
</body>
③、将频繁DOM操作改为一次性操作
这里面要理解一个概念DOM文档片段(虚拟节点对象)文档片段的作用是充当其它要被添加到文档的节点的仓库 。他自己永远不会被添加到文档树中,但是他能包含和操作节点。可以通过
document.createDocumentFragment()方法来创建文档片段。
具体的代码实现
<body>
<ul id="list"></ul>
<script>
console.time("优化后");
const oUl = document.getElementById("list");
//创建一个文档片段,些时还没有插入到DOM中,存在内存中
const frag = document.createDocumentFragment();
//执行行入操作
for (let x = 0; x < 10000; x++) {
const li = document.createComment("li");
//将DOM先放到文档片段中,这样不会频繁操作DOM
frag.appendChild(li);
//最后一次性将10000个li插入到DOM树中
oUl.appendChild(frag);
console.timeEnd("优化后");
</script>
</body>
这个优化消耗的时间需要在正常的网页上去测试才能看到效果,因为如果页面中没有其它元素,也就不会造成重排和重绘,那对于时间上的消耗也是看不到的。大家可以自己行测试。
④ 操作DOM前,先把DOM节点删除或隐藏
如果要对一个元素进行多次DOM操作,可以先将其隐藏,操作完成后再显示。这样只在隐藏和显示时触发2次重排,而不会是在每次进行操作时都出发一次重排。因为时的元素不在渲染树中,因此对隐藏的元素操作不会引发其他元素的重排。
<body>
<ul id="list"></ul>
<script>
var oUl = document.getElementById("list");
oUl.style.display = "none";/*先隐藏元素*/
for (var i = 0; i < 15000; i++) {
var li = document.createElement("li");
oUl.appendChild(li);
oUl.style.display = "block";/*DOM操作完再显示*/
</script>
</body>
⑤ 事件代理
事件委托,利用浏览器事件冒泡捕获减少页面事件绑定,我们可以指定一个事件处理程序就可以管理某一类型的所有事件。事件函数过多会占用大量内存,而且绑定事件的DOM元素越多会增加访问dom的次数,对页面的交互就绪时间也会有延迟。
<body>
<ul id="list">
<li>1</li>
<li>2</li>
<li>3</li>
<li>4</li>
<script>
//事件委托
var oUl = document.getElementById("list");
oUl.onclick = function (e) {
console.log(e.target.innerHTML);
/* 未用事件委托
var li = document.querySelectorAll("#list li");
var n = li.length;
for (let i = 0; i < n; i++) {