JavaScript之优化DOM
优化DOM得从重绘和重排讲起,long long ago...
1、重绘和重排
1.1 重绘和重排是什么
重绘是指一些样式的修改,元素的位置和大小都没有改变;
重排是指元素的位置或尺寸发生了变化,浏览器需要重新计算渲染树,而新的渲染树建立后,浏览器会重新绘制受影响的元素。
1.2 浏览器渲染页面
去参加面试总会被问到一个问题,那就是“向浏览器输入一行url会发生什么?”,这个问题的答案除了要回答网络方面的知识还牵扯到浏览器渲染页面问题。当我们的浏览器接收到从服务器响应的页面之后便开始逐行渲染,遇到css的时候会异步的去计算属性值,再继续向下解析dom解析完毕之后形成一颗DOM树,将异步计算好的样式(样式盒子)与DOM树相结合便成为了一个Render树,再由浏览器绘制在页面上。DOM树与Render树的区别在于:样式为display:none;的节点会在DOM树中而不在渲染树中。浏览器绘制了之后便开始解析js文件,根据js来确定是否重绘和重排。
1.3 引起重绘和重排的原因
产生重绘的因素:
- 改变visibility、outline、背景色等样式属性,并没有改变元素大小、位置等。浏览器会根据元素的新属性重新绘制。
产生重排的因素:
- 内容改变
- 文本改变或图片尺寸改变
- DOM元素的几何属性的变化
- 例如改变DOM元素的宽高值时,原渲染树中的相关节点会失效,浏览器会根据变化后的DOM重新排建渲染树中的相关节点。如果父节点的几何属性变化时,还会使其子节点及后续兄弟节点重新计算位置等,造成一系列的重排。
- DOM树的结构变化
- 添加DOM节点、修改DOM节点位置及删除某个节点都是对DOM树的更改,会造成页面的重排。浏览器布局是从上到下的过程,修改当前元素不会对其前边已经遍历过的元素造成影响,但是如果在所有的节点前添加一个新的元素,则后续的所有元素都要进行重排。
- 获取某些属性
- 除了渲染树的直接变化,当获取一些属性值时,浏览器为取得正确的值也会发生重排,这些属性包括:
offsetTop、offsetLeft、offsetWidth、offsetHeight、scrollTop、scrollLeft、scrollWidth、scrollHeight、clientTop、clientLeft、clientWidth、clientHeight、getComputedStyle()。
- 除了渲染树的直接变化,当获取一些属性值时,浏览器为取得正确的值也会发生重排,这些属性包括:
- 浏览器窗口尺寸改变
- 窗口尺寸的改变会影响整个网页内元素的尺寸的改变,即DOM元素的集合属性变化,因此会造成重排。
- 滚动条的出现(会触发整个页面的重排)
总之你要知道,js是单线程的,重绘和重排会阻塞用户的操作以及影响网页的性能,当一个页面发生了多次重绘和重排比如写一个定时器每500ms改变页面元素的宽高,那么这个页面可能会变得越来越卡顿,我们要尽可能的减少重绘和重排。那么我们对于DOM的优化也是基于这个开始。
2、优化
2.1 减少访问
减少访问次数自然是想到缓存元素,但是要注意
var ele = document.getElementById('ele');
这样并不是对ele进行缓存,每一次调用ele还是相当于访问了一次id为ele的节点。
2.1.1 缓存NodeList
var foods = document.getElementsByClassName('food');
我们可以用foods[i]来访问第i个class为food的元素,不过这里的foods并不是一个数组,而是一个NodeList。NodeList是一个类数组,保存了一些有序的节点并可以通过位置来访问这些节点。NodeList对象是动态的,每一次访问都会运行一次基于文档的查询。所以我们要尽量减少访问NodeList的次数,可以考虑将NodeList的值缓存起来。
// 优化前
var lis = document.getElementsByTagName('li'); for(var i = 0; i < lis.length; i++) {
// do something...
} // 优化后,将length的值缓存起来就不会每次都去查询length的值
var lis = document.getElementsByTagName('li'); for(var i = 0, len = lis.length; i < len; i++) {
// do something...
}
而且由于NodeList是动态变化的,所以如果不缓存可能会引起死循环,比如一边添加元素,一边获取NodeList的length。
2.1.2 改变选择器
获取元素最常见的有两种方法,getElementsByXXX()和queryselectorAll(),这两种选择器区别是很大的,前者是获取动态集合,后者是获取静态集合,举个例子。
// 假设一开始有2个li
var lis = document.getElementsByTagName('li'); // 动态集合
var ul = document.getElementsByTagName('ul')[0]; for(var i = 0; i < 3; i++) {
console.log(lis.length);
var newLi = document.createElement('li');
ul.appendChild(newLi);
}
// 输出结果:2, 3, 4 var lis = document.querySelector('li'); // 静态集合
var ul = document.getElementsByTagName('ul')[0]; for(var i = 0; i < 3; i++) {
console.log(lis.length);
var newLi = document.createElement('li');
ul.appendChild(newLi);
}
// 输出结果:2, 2, 2
对静态集合的操作不会引起对文档的重新查询,相比于动态集合更加优化。
2.1.3 避免不必要的循环
// 优化前
for(var i = 0; i < 10; i++) {
document.getElementById('ele').innerHTML += 'a';
} // 优化后
var str = '';
for(var i = 0; i < 10; i++) {
str += 'a';
}
document.getElementById('ele').innerHTML = str;
优化前的代码访问了10次ele元素,而优化后的代码只访问了一次,大大的提高了效率。
2.1.4 事件委托
js中的事件函数都是对象,如果事件函数过多会占用大量内存,而且绑定事件的DOM元素越多会增加访问dom的次数,对页面的交互就绪时间也会有延迟。所以诞生了事件委托,事件委托是利用了事件冒泡,只指定一个事件处理程序就可以管理某一类型的所有事件。
// 事件委托前
var lis = document.getElementsByTagName('li');
for(var i = 0; i < lis.length; i++) {
lis[i].onclick = function() {
console.log(this.innerHTML);
};
} // 事件委托后
var ul = document.getElementsByTagName('ul')[0];
ul.onclick = function(event) {
console.log(event.target.innerHTML);
};
事件委托前我们访问了lis.length次li,而采用事件委托之后我们只访问了一次ul。
2.2 减少重绘重排
2.2.1 改变一个dom节点的多个样式
我们想改变一个div元素的宽度和高度,通常做法可以是这样
var div = document.getElementById('div1');
div.style.width = '220px';
div.style.height = '300px';
以上操作改变了元素的两个属性,访问了三次dom,触发两次重排与两次重绘。我们说过优化是减少访问次数以及减少重绘重排次数,从这个出发点可不可以只访问一次元素以及重排次数降低到1呢?显然是可以的,我们可以在css里写一个class
/* css
.change {
width: 220px;
height: 300px;
}
*/
document.getElementById('div').className = 'change';
这样就达到了一次操作多个样式
2.2.2 批量修改dom节点样式
上面代码的情况是针对于一个dom节点的,如果我们要改变一个dom集合的样式呢?
第一时间想到的方法是遍历集合,给每个节点加一个className。再想想这样岂不是访问了多次dom节点?想想文章开头说的dom树和渲染树的区别,如果一个节点的display属性为none那么这个节点不会存在于render树中,意味着对这个节点的操作也不会影响render树进而不会引起重绘和重排,基于这个思路我们可以实现优化:
- 将待修改的集合的父元素display: none;
- 之后遍历修改集合节点
- 将集合父元素display: block;
// 假设增加的class为.change
var lis = document.getElementsByTagName('li');
var ul = document.getElementsByTagName('ul')[0]; ul.style.display = 'none'; for(var i = 0; i < lis.length; i++) {
lis[i].className = 'change';
} ul.style.display = 'block';
3、总结
- 减少访问dom的次数
- 缓存节点属性值
- 选择器的使用
- 避免不必要的循环
- 事件委托
- 减少重绘与重排
- 使用className改变多个样式
- 使父元素脱离文档流再恢复
如果以后看到其他优化方案我会更新,欢迎大家与我交流。
参考文档:
JavaScript之优化DOM的更多相关文章
- JavaScript性能优化 DOM编程
最近在研读<高性能JavaScript>,在此做些简单记录.示例代码可在此处查看到. 一.DOM 1)DOM和JavaScript 文档对象模型(DOM)是一个独立于语言的,用于操作XML ...
- JavaScript性能优化
如今主流浏览器都在比拼JavaScript引擎的执行速度,但最终都会达到一个理论极限,即无限接近编译后程序执行速度. 这种情况下决定程序速度的另一个重要因素就是代码本身. 在这里我们会分门别类的介绍J ...
- 前端工程优化:javascript的优化小结
我觉得优化javascript是一门高深的学问,在这里也只能站在前人的肩膀上,说一些我浅显的认识,更希望的是抛钻引玉,如有不对,敬请斧正. 首先,要认识到是,优化js的关键之处在于,优化它的运行速度 ...
- javascript性能优化-repaint和reflow
repaint(重绘) ,repaint发生更改时,元素的外观被改变,且在没有改变布局的情况下发生,如改变outline,visibility,background color,不会影响到dom结构渲 ...
- 摘:JavaScript性能优化小知识总结
原文地址:http://www.codeceo.com/article/javascript-performance-tips.html JavaScript的性能问题不容小觑,这就需要我们开发人员在 ...
- 优化DOM交互
DOM操作与交互要消耗大量时间,所以优化DOM交互有重大意义. 1.最小化现场更新 如果需要访问的DOM部分是已经显示的页面的一部分,那么这就是在进行一个现场更新.现场更新得越多,代码完成执行所需要的 ...
- javascript系列之DOM(一)
原文:javascript系列之DOM(一) DOM(document object moudle),文档对象模型.它是一个中立于语言的应用程序接口(API),允许程序访问并修改文档的结构,内容和样式 ...
- javascript系列之DOM(二)
原文:javascript系列之DOM(二) 原生DOM扩展 我们接着第一部分来说,上文提到了两种常规的DOM操作:创建文档片段和遍历元素节点.我们知道那些雨后春笋般的库,有很大一部分工作就是提供了一 ...
- JavaScript性能优化小知识总结
原文出处: YouYaInsist 欢迎分享原创到伯乐头条 前言 一直在学习javascript,也有看过<犀利开发Jquery内核详解与实践>,对这本书的评价只有两个字犀利,可能是对 ...
随机推荐
- [翻译] 编写高性能 .NET 代码--第二章 GC -- 将长生命周期对象和大对象池化
将长生命周期对象和大对象池化 请记住最开始说的原则:对象要么立即回收要么一直存在.它们要么在0代被回收,要么在2代里一直存在.有些对象本质是静态的,生命周期从它们被创建开始,到程序停止才会结束.其它对 ...
- 利用mock提高效率
利用mock提高效率 谈到mock,就不得不讲前后端分离.理想情况下前后端不分离,由全栈的人以product和infrastructure的维度进行开发,效率是最高的.近些年来业务的复杂度越来越高,真 ...
- 传统IO与NIO(channel-to-channel)文件拷贝的探索与性能比对
Channel-to-channel传输是可以极其快速的,特别是在底层操作系统提供本地支持的时候.某些操作系统可以不必通过用户空间传递数据而进行直接的数据传输.对于大量的数据传输,这会是一个巨大的帮助 ...
- Web API 之承载宿主IIS,SelfHost,OwinSelfHost
Asp.Net WebAPI这个大家应该都不陌生,在我的理解范围中就是数据提供和交换的一个方式,相比与WCF,WS而言,更加的简单轻量,但是在部署web Api的时候,一般往往需要与a ...
- mysql3 - 常规数据检索、常见操作与函数
一.常规数据检索 二.常见操作与函数
- java3 - 流程控制
一.Java 有三种主要的循环结构: 需求:分别使用三种循环将 1 到 100 的整数输出到控制台. 1.for 循环 for(初始化语句; 布尔表达式语句; 更新语句) { //循环体内容 } 示列 ...
- Qt 开发 MS VC 控件终极篇
Qt 开发 MS VC 控件终极篇 1. 使用 MSVC2015 通过项目向导创建 Qt ActiveQt Server 解决方案 项目配置:以下文件需要修改 1. 项目属性页->项目属性-&g ...
- 《android开发艺术探索》读书笔记(十四)--JNI和NDK编程
接上篇<android开发艺术探索>读书笔记(十三)--综合技术 No1: Java JNI--Java Native Interface(java本地接口),它是为了方便java调用C. ...
- C语言老司机学Python (二)
标准数据类型: 共6种:Number(数字),String(字符串),List(列表),Tuple(元组),Sets(集合),Dictionary(字典) 本次学习主要是和数据类型混个脸熟,知道每样东 ...
- DTCMS插件的制作实例电子资源管理(四)URL重写
总目录 插件目录结构(一) Admin后台页面编写(二) 前台模板页编写(三) URL重写(四) 本实例旨在以一个实际的项目中的例子来介绍如何在dtcms中制作插件,本系列文章非入门教程,部分逻辑实现 ...