用 JS 进行 DOM 操作的代价是昂贵的, 它是富 web 应用中最常见的性能瓶颈.
DOM
文档对象模型 (DOM) 是一个独立于语言的, 用于操作 xml 和 html 文档的程序接口(API). 通常在浏览器中 DOM 和 JS 都是独立的, 因为彼此独立, 所以 JS 操作 DOM, 性能开销就很大.
提升性能最佳实践
减少操作
典型场景, 循环操作 DOM, 改成循环拼接, 最后操作 DOM.
- // 循环操作 DOM15000 次
- function innerHTMLLoop(){
- for(var count = 0; count <15000; count++){
- document.getElementById('here').innerHTML += 'a';
- }
- }
- // 只操作 1 次 DOM, 在 IE8 中性能提升 273 倍
- function innerHTMLLoop(){
- var content = '';
- for(var count = 0; count < 15000; count++){
- content += 'a';
- }
- document.getElementById('here').innerHTML = content;
- }
增加新元素时 innerHTML 性能高于 DOM 方法
- var newDiv = "<div></div>";
- document.getElementById('here').innerHTML = newDiv;
- //
- var newElement = document.createElement('div');
- document.getElementById('here').appendChild(newElement);
使用节点 clone
- var newElement1 = document.createElement('div');
- var newElementN = newElement1.cloneNode(true);
HTML 集合使用 array 代替
DOM 查询方法 (getElementByName,getElementByClassName,getElementByTagName), 以及部分属性(images,links,forms,elements) 返回值是 HTML 集合(类似数组, 有 length 和下标访问, 无 push,slice). 这些集合要避免重复访问, 因为每次访问都会重新执行查询.
- // 由于每次添加 div 后, 长度增加, 此循环是死循环
- var divList= document.getElementByName('div');
- for(var count = 0; count <divList.length; count++){
- document.body.appendChild(document.createElement('div'));
- }
- // 集合保存到变量, 避免重复查询
- for(var count = 0; count < document.getElementByName('div').length; count++){
- //do something
- }
- var divList= document.getElementByName('div');
- var len = divList.length
- var arr = toArray(divList); //toArray 是自定义的函数, 将集合转成 array
- for(var count = 0; count < len; count++){
- //do something
- }
注意: 此方法会额外增加一次遍历操作, 长度小的集合可能不会提升反而下降
局部变量替代
- for(var i = 0; i < document.getElementsByTagName("a").length; i++){
- document.getElementsByTagName("a")[i].class = 'active'
- }
- // 改进后
- var list = document.getElementsByTagName("a");
- var len = list.length;
- for(var i = 0; i < len; i++){
- list[i].class = 'active'
- }
IE6,7 中遍历 DOM,nextSibling 性能高于 childNode
遍历元素节点优选元素节点属性
childNodes,firstChild 和 nextSibling 这些属性并不区分元素节点和其他类型节点(比如注释和文本节点). 如果只需要查询元素节点, 优先使用如下方法代替.(注意: IE6-8 只支持 children 属性)
元素节点属性名 | 被替代的属性名 |
---|---|
children | childNodes |
childElementCount | childNodes.length |
firstElementChild | firstChild |
lastElementChild | lastChild |
nextElementSibling | nextSibling |
previousElementSibling | previousSibling |
利用 CSS 选择器提高查找效率
querySelectorAll()方法使用 CSS 选择器作为参数, 并且返回匹配节点的类数组对象. 不会返回 HTML 集合, 不会对应实时的文档结构, 避免了之前讨论的 HTML 集合引起的性能和逻辑问题. 代码示例如下:
- // 改进前
- var els = document.getElementsById("menu").getElementsByTagName("a");
- // 改进后
- var els = document.querySelectorAll("#menu a");
如果是组合查询, querySelectorAll()方法更具优势. 对比一下:
- // 改进前
- var els = [];
- var divs = document.getElementsByTagName("div");
- var className = "";
- for(var i = 0,len = divs.length; i<len;i++){
- className = divs[i].className;
- if(className === 'warning' || className === 'notice'){
- els.push(divs[i]);
- }
- }
- // 改进后
- var els = document.querySelectorAll("div.warning, div.notice");
推荐使用 querySelector()方法, 查询第一个匹配的节点.
重绘与重排
浏览器下载完所有的组件文件 (HTML,JS,CSS, 图片) 之后, 会解析这些组件, 并生成两个数据结构:
DOM 树: 表示页面结构
渲染树: 表示 DOM 节点如何显示
DOM 树中的每一个需要显示的节点在渲染树中至少存在一个对应的节点 (隐藏的 DOM 元素在渲染树中没有对应的节点). 渲染树中的节点被称为 "帧" 或者 "盒", 具有内边距 padding, 外边距 margin, 边框 border 和位置 position(IE 盒模型的高度和宽度包括边框和内边距, W3C 只是内容部分. W3C 盒模型可使用 box-sizing:border-box 改成 IE 盒模型). 一旦 DOM 树和渲染树构建完成, 浏览器就开始显示(绘制) 页面元素.
当 DOM 的变化影响了元素的几何属性(宽和高), 浏览器需要重新计算元素的集合属性, 同事其他元素的集合属性和位置也会受到影响. 浏览器会使渲染树中受到影响的部分失效, 并重新构造渲染树. 这个过程被称为 "重排 reflow". 完成重排后, 浏览器会重新绘制受影响的部分到平路中, 该过程被称为 "重绘 repaint".
并不是所有的 DOM 变化都会影响几何属性, 比如改变背景色, 此时只会执行重绘而不会触发重排. 重绘和重排都是代价昂贵的操作, 需要尽量避免.
触发重排的操作
添加和删除可见的 DOM 元素
元素位置改变
元素尺寸变化
内容改变
页面渲染器初始化
浏览器窗口尺寸改变
滚动条的出现和消失会触发整个页面的重排
渲染树变化的排队与刷新
由于重排消耗大, 大多数浏览器都会通过队列化修改并批量执行来优化重排过程. 获取布局的如下操作会导致队列刷新:
- offsetTop,offsetLeft,offsetWidth,offsetHeight
- scrollTop,scrollLeft,scrollWidth,scrollHeight
- clientTop,clientLeft,clientWidth,clientHeight
- getCumputedStyle()(currentStyle in IE)
执行这些属性和方法需要返回最新的布局信息, 因此浏览器会执行渲染队列中的操作, 已获得最新的布局信息. 因此不需要避免频繁执行这些属性和方法.
最小化重排和重绘
为减少重排或者重绘, 应该合并多次对 DOM 和样式的修改, 然后一次性处理.
修改样式
- // 优化前, 执行了三次重排. 大部分现代浏览器进行了优化, 可能只执行一次
- var el = document.getElementById('mydiv');
- el.style.borderLeft='1px';
- el.style.borderRight='2px';
- el.style.padding='5px';
- // 优化后, 只执行一次
- var el = document.getElementById('mydiv');
- el.style.ccsText='border-left:1px;border-right:2px;padding:5px;';
- // 第二种优化方法
- var el = document.getElementById('mydiv');
- el.className='active';
批量修改 DOM
可以通过如下步骤减少重绘和重排次数:
使元素脱离文档流
对其应用多重改变
把元素待会文档中
这样操作后只会在 1 和 3 补执行两次重排, 忽略了步骤 2 中可能的 N 次重排.
使元素脱离文档流的方法有如下三种:
隐藏元素, 应用修改, 重新显示
使用文档片段在当前 DOM 之外构建一个子树, 执行完修改后再把它拷贝回文档
讲原始元素拷贝到一个脱离文档的节点中, 修改这个副本, 完成后再替换原始元素.
- function appendDataToElement(appendToElement, data) {
- var a, li;
- for (var i = 0, max = data.length; i < max; i++) {
- a = document.createElement('a');
- a.href = data[i].url;
- a.appendChild(document.createTextNode(data[i].name));
- li = document.createElement('li');
- li.appendChild(a);
- appendToElement.appendChild(li);
- }
- };
- // 优化前, 循环内 N 次重排
- var ul = document.getElementById('mylist');
- appendDataToElement(ul, data);
- // 第一种, 异常和显示
- var ul = document.getElementById('mylist');
- ul.style.display = 'none';
- appendDataToElement(ul, data);
- ul.style.display = 'block';
- // 第二种, 文档片段
- var fragment = document.createDocumentFragment();
- appendDataToElement(fragment, data);
- document.getElementById('mylist').appendChild(fragment);
- // 第三种, 元素替换
- var old = document.getElementById('mylist');
- var clone = old.cloneNode(true);
- appendDataToElement(clone, data);
- old.parentNode.replaceChild(clone, old);
推荐第二种方案, 其产生的 DOM 遍历和重排次数最少.
缓存布局信息
当查询布局信息(例如 offsets,scroll,client 等), 浏览器为返回最新值, 会刷新队列并应用所有变更. 所哟尽量减少布局信息的获取次数, 获取后赋值给局部变量, 然后再操作局部变量.
例如移动元素的例子, timeout 循环部分
- // 改进前
- myElement.style.left = 1 + myElement.offsetLeft + 'px';
- myElement.style.top = 1 + myElement.offsetTop + 'px';
- if (myElement.offsetLeft>= 500) {
- stopAnimation();
- }
- // 改进后, 先一次性获取初始位置
- var current = myElement.offsetLeft;
- // 然后循环执行操作
- current++
- myElement.style.left = current + 'px';
- myElement.style.top = current + 'px';
- if (current>= 500) {
- stopAnimation();
- }
让元素脱离动画流
采用绝对位置定位, 可以减少元素尺寸变化时, 对其他元素造成的重排影响.
例如折叠 / 展开这种交互方式, 每次变化都会导致下方所有元素的移动. 如果把这部分元素使用绝对位置定位, 覆盖其他部分. 这样就能避免下方元素的重排和重绘, 减少开销.
IE 和: hover
从 IE7 开始, IE 允许任何元素上使用: hover 这个 CSS 伪选择器. 如果大量使用: hover, 响应速度下降明显. 特别是 IE8.
事件委托
如果进行大量的 DOM 元素事件绑定, 会引入性能问题. 一个简单的解决方案是事件委托. 只需给最外层的元素绑定事件, 利用事件逐层冒泡并能被父级元素捕获, 就可以处理所有子元素上触发的事件.
来源: http://www.jianshu.com/p/cccb0bf03f3d