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)
注意点:
- 样式为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; padding: 0px; border: 0px; color: rgb(197, 134, 192); --tt-darkmode-color: #C586C0;">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次重排,而不会是在每次进行操作时都出发一次重排。因为display:none时的元素不在渲染树中,因此对隐藏的元素操作不会引发其他元素的重排。
<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>
</ul>
<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++) {
li[i].onclick = function () {
console.log(this.innerHTML);
};
} */
</script>
</body>
DOM是JS阶段重要的教学内容,在星辰班已经讲了40个小节的课程,更多完整JavaScript课程体系在我们的系统班里有完整的呈现,包含了JavaScript基础篇、重点、算法、原理、面试题、实战案例讲解!同时也为你提供了前端高级工程师成长体系!(详细看下图内容)
如果需要深度学习的同学可以联系助理老师了解详细的课程以及课程的报名方式!(不定期会推出活动,有大额优惠券推出,活动详情联系助理老师了解即可!)
如果你才开始学习前端,那么可以先学习我们的三十天计划(零基础的同学报名系统班同学可以和老师沟通制定学习计划,可以得到更快的成长!)
为帮助到一部分同学不走弯路,真正达到一线互联网大厂前端项目研发要求,首次实力宠粉,打造了《30天挑战学习计划》,内容如下:
HTML/HTML5,CSS/CSS3,JavaScript,真实企业项目开发,云服务器部署上线,从入门到精通
- PC端项目开发(1个)
- 移动WebApp开发(2个)
- 多端响应式开发(1个)
共4大完整的项目开发 !一行一行代码带领实践开发,实际企业开发怎么做我们就是怎么做。从学习一开始就进入工作状态,省得浪费时间。
从学习一开始就同步使用 Git 进行项目代码的版本的管理,Markdown 记录学习笔记,包括真实大厂项目的开发标准和设计规范,命名规范,项目代码规范,SEO优化规范
从蓝湖UI设计稿 到 PC端,移动端,多端响应式开发项目开发
- 真机调试,云服务部署上线;
- Linux环境下 的 Nginx 部署,Nginx 性能优化;
- Gzip 压缩,HTTPS 加密协议,域名服务器备案,解析;
- 企业项目域名跳转的终极解决方案,多网站、多系统部署;
- 使用 使用 Git 在线项目部署;
这些内容在《30天挑战学习计划》中每一个细节都有讲到,包含视频+图文教程+项目资料素材等。只为实力宠粉,真正一次掌握企业项目开发必备技能,不走弯路 !
过程中【不涉及】任何费用和利益,非诚勿扰 。
如果你没有添加助理老师微信,可以添加下方微信,说明要参加30天挑战学习计划,来自头条号!老师会邀请你进入学习,并给你发放相关资料。