Web前端开发最佳实践(13):前端页面卡顿?可能是DOM操作惹的祸,你需要优化代码
文档对象模型(DOM)是一个独立于特定语言的应用程序接口。在浏览器中,DOM接口是以JavaScript语言实现的,通过JavaScript来操作浏览器页面中的元素,这使得DOM成为了JavaScript中重要的组成部分。在富客户端网页应用中,界面上UI的更改都是通过DOM操作实现的,并不是通过传统的刷新页面实现的。尽管DOM提供了丰富接口供外部调用,但DOM操作的代价很高,页面前端代码的性能瓶颈也大多集中在DOM操作上,所以前端性能优化的一个主要的关注点就是DOM操作的优化。DOM操作优化的总原则是尽量减少DOM操作。
在讨论DOM操作的最佳性能实践之前,先来看看DOM操作为什么会影响性能。在浏览器中,DOM的实现和ECMAScript的实现是分离的。比如在IE中,ECMAScrit的实现在jscript.dll中,而DOM的实现在mshtml.dll中;在Chrome中使用WebKit中的WebCore处理DOM和渲染,但ECMAScript是在V8引擎中实现的,其他浏览器的情况类似。所以通过JavaScript代码调用DOM接口,相当于两个独立模块的交互。相比较在同一模块中的调用,这种跨模块的调用其性能损耗是很高的。但DOM操作对性能影响最大其实还是因为它导致了浏览器的重绘(repaint)和重排(reflow)。
为了让大家能更深刻地理解重绘和重排对性能的影响,这里需要简单叙述一下浏览器的渲染原理(如果想详细了解浏览器的工作原理,请参照文章《浏览器的工作原理:新式网络浏览器幕后揭秘》 )。从下载文档到渲染页面的过程中,浏览器会通过解析HTML文档来构建DOM树,解析CSS产生CSS规则树。JavaScript代码在解析过程中,可能会修改生成的DOM树和CSS规则树。之后根据DOM树和CSS规则树构建渲染树,在这个过程中CSS会根据选择器匹配HTML元素。渲染树包括了每个元素的大小、边距等样式属性,渲染树中不包含隐藏元素及head元素等不可见元素。最后浏览器根据元素的坐标和大小来计算每个元素的位置,并绘制这些元素到页面上。重绘指的是页面的某些部分要重新绘制,比如颜色或背景色的修改,元素的位置和尺寸并没用改变;重排则是元素的位置或尺寸发生了改变,浏览器需要重新计算渲染树,导致渲染树的一部分或全部发生变化。渲染树重新建立后,浏览器会重新绘制页面上受影响的元素。重排的代价比重绘的代价高很多,重绘会影响部分的元素,而重排则有可能影响全部的元素。如下的这些DOM操作会导致重绘或重排:
增加、删除和修改可见DOM元素
页面初始化的渲染
移动DOM元素
修改CSS样式,改变DOM元素的尺寸
DOM元素内容改变,使得尺寸被撑大
浏览器窗口尺寸改变
浏览器窗口滚动
可以看出,这些操作都是DOM操作中比较常见的。现代浏览器会针对重排或重绘做性能优化,比如,把DOM操作积累一批后统一做一次重排或重绘。但在有些情况下,浏览器会立即重排或重绘。比如请求如下的DOM元素布局信息:offsetTop/Left/Width/Height
、scrollTop/Left/Width/Height
、clientTop/Left/Width/Height
、getComputedStyle()
或 currentStyle
。因为这些值都是动态计算的,所以浏览器需要尽快完成页面的绘制,然后计算返回值,从而打乱了重排或重绘的优化。
DOM操作带来的页面重绘或重排是不可避免的,但可以遵循一些最佳实践来降低由于重排或重绘带来的影响。如下是一些具体的实践方法:
1. 合并多次的DOM操作为单次的DOM操作
最常见频繁进行DOM操作的是频繁修改DOM元素的样式,代码类似如下:
element.style.borderColor = '#f00';
element.style.borderStyle = 'solid';
element.style.borderWidth = '1px';
这种编码方式会因为频繁更改DOM元素的样式,触发页面多次的重排或重绘,上面介绍过,现代浏览器针对这种情况有性能的优化,它会合并DOM操作,但并不是所有的浏览器都存在这样的优化。推荐的方式是把DOM操作尽量合并,如上的代码可以优化为:
// 优化方案1
element.style.cssText += 'border: 1px solid #f00;';
// 优化方案2
element.className += 'empty';
示例的代码有两种优化的方案,都做到了把多次的样式设置合并为一次设置。方案2比方案1稍微有一些性能上的损耗,因为它需要查询CSS类。但方案2的维护性最好,这在上一章曾经讨论过。很多时候,如果性能问题并不突出,选择编码方案时需要优先考虑的是代码的维护性。
类似的操作还有通过innerHTML接口修改DOM元素的内容。不要直接通过此接口来拼接HTML代码,而是以字符串方式拼接好代码后,一次性赋值给DOM元素的innerHTML
接口。
2. 把DOM元素离线或隐藏后修改
把DOM元素从页面流中脱离或隐藏,这样处理后,只会在DOM元素脱离和添加时,或者是隐藏和显示时才会造成页面的重绘或重排,对脱离了页面布局流的DOM元素操作就不会导致页面的性能问题。这种方式适合那些需要大批量修改DOM元素的情况。具体的方式主要有三种:
(1)使用文档片段
文档片段是一个轻量级的document对象,并不会和特定的页面关联。通过在文档片段上进行DOM操作,可以降低DOM操作对页面性能的影响,这种方式是创建一个文档片段,并在此片段上进行必要的DOM操作,操作完成后将它附加在页面中。对页面性能的影响只存在于最后把文档片段附加到页面的这一步操作上。代码类似如下:
var fragment = document.createDocumentFragment();
// 一些基于fragment的大量DOM操作
...
document.getElementById('myElement').appendChild(fragment);
(2)通过设置DOM元素的display样式为none来隐藏元素
这种方式是通过隐藏页面的DOM元素,达到在页面中移除元素的效果,经过大量的DOM操作后恢复元素原来的display样式。对于这类会引起页面重绘或重排的操作,就只有隐藏和显示DOM元素这两个步骤了。代码类似如下:
var myElement = document.getElementById('myElement');
myElement.style.display = 'none';
// 一些基于myElement的大量DOM操作
...
myElement.style.display = 'block';
(3)克隆DOM元素到内存中
这种方式是把页面上的DOM元素克隆一份到内存中,然后再在内存中操作克隆的元素,操作完成后使用此克隆元素替换页面中原来的DOM元素。这样一来,影响性能的操作就只是最后替换元素的这一步操作了,在内存中操作克隆元素不会引起页面上的性能损耗。代码类似如下:
var old = document.getElementById('myElement');
var clone = old.cloneNode(true);
// 一些基于clone的大量DOM操作
...
old.parentNode.replaceChild(clone, old);
在现代的浏览器中,因为有了DOM操作的优化,所以应用如上的方式后可能并不能明显感受到性能的改善。但是在仍然占有市场的一些旧浏览器中,应用以上这三种编码方式则可以大幅提高页面渲染性能。
3. 设置具有动画效果的DOM元素的position属性为fixed或absolute
把页面中具有动画效果的元素设置为绝对定位,使得元素脱离页面布局流,从而避免了页面频繁的重排,只涉及动画元素自身的重排了。这种做法可以提高动画效果的展示性能。如果把动画元素设置为绝对定位并不符合设计的要求,则可以在动画开始时将其设置为绝对定位,等动画结束后恢复原始的定位设置。在很多的网站中,页面的顶部会有大幅的广告展示,一般会动画展开和折叠显示。如果不做性能的优化,这个效果的性能损耗是很明显的。使用这里提到的优化方案,则可以提高性能。
4. 谨慎取得DOM元素的布局信息
前面讨论过,获取DOM的布局信息会有性能的损耗,所以如果存在重复调用,最佳的做法是尽量把这些值缓存在局部变量中。考虑如下的一个示例:
for (var i=0; i < len; i++) {
myElements[i].style.top = targetElement.offsetTop + i*5 + 'px';
}
如上的代码中,会在一个循环中反复取得一个元素的offsetTop
值,事实上,在此代码中该元素的offsetTop
值并不会变更,所以会存在不必要的性能损耗。优化的方案是在循环外部取得元素的offsetTop值,相比较之前的方案,此方案只是调用了一遍元素的offsetTop
值。更改后的代码如下:
var targetTop = targetElement.offsetTop;
for (var i=0; i < len; i++) {
myElements[i].style.top = targetTop+ i*5 + 'px';
}
另外,因为取得DOM元素的布局信息会强制浏览器刷新渲染树,并且可能会导致页面的重绘或重排,所以在有大批量DOM操作时,应避免获取DOM元素的布局信息,使得浏览器针对大批量DOM操作的优化不被破坏。如果需要这些布局信息,最好是在DOM操作之前就取得。考虑如下一个示例:
var newWidth = div1.offsetWidth + 10;
div1.style.width = newWidth + 'px';
var newHeight = myElement.offsetHeight + 10; // 强制页面重排
myElement.style.height = newHeight + 'px'; // 又会重排一次
根据上面的介绍,代码在遇到取得DOM元素的信息时会触发页面重新计算渲染树,所以如上的代码会导致页面重排两次,如果把取得DOM元素的布局信息提前,因为浏览器会优化连续的DOM操作,所以实际上只会有一次的页面重排出现,优化后的代码如下:
var newWidth = div1.offsetWidth + 10;
var newHeight = myElement.offsetHeight + 10;
div1.style.width = newWidth + 'px';
myElement.style.height = newHeight + 'px';
5. 使用事件托管方式绑定事件
在DOM元素上绑定事件会影响页面的性能,一方面,绑定事件本身会占用处理时间,另一方面,浏览器保存事件绑定,所以绑定事件也会占用内存。页面中元素绑定的事件越多,占用的处理时间和内存就越大,性能也就相对越差,所以在页面中绑定的事件越少越好。一个优雅的手段是使用事件托管方式,即利用事件冒泡机制,只在父元素上绑定事件处理,用于处理所有子元素的事件,在事件处理函数中根据传入的参数判断事件源元素,针对不同的源元素做不同的处理。这样就不需要给每个子元素都绑定事件了,管理的事件绑定数量变少了,自然性能也就提高了。这种方式也有很大的灵活性,可以很方便地添加或删除子元素,不需要考虑因元素移除或改动而需要修改事件绑定。示例代码如下:
// 获取父节点,并添加一个click事件
document.getElementById('list').addEventListener("click",function(e) {
// 检查事件源元素
if(e.target && e.target.nodeName.toUpperCase == "LI") {
// 针对子元素的处理
...
}
});
上述代码中,只在父元素上绑定了click事件,当点击子节点时,click事件会冒泡,父节点捕获事件后通过e.target检查事件源元素并做相应地处理。
在JavaScript中,事件绑定方式存在浏览器兼容问题,所以在很多框架中也提供了相似的接口方法用于事件托管。比如在jQuery中可以使用如下方式实现事件的托管(示例代码来自jQuery官方网站):
$( "table" ).on( "click", "td", function() {
$( this ).toggleClass( "chosen" );
});
Web前端开发最佳实践(13):前端页面卡顿?可能是DOM操作惹的祸,你需要优化代码的更多相关文章
- 前端页面卡顿?或是DOM操作惹的祸,需优化代码
文档对象模型(DOM)是一个独立 于特定语言的应用程序接口.在浏览器中,DOM接口是以JavaScript语言实现的,通过JavaScript来操作浏览器页面中的元素,这使得 DOM成为了JavaSc ...
- Web前端开发最佳实践(4):在页面中添加必要的meta信息
meta标签放置在HTML页面的head中,主要用于标识网站.其中基本上包含了网站的一些描述信息,例如,简介.作者等.这些信息有助于搜索引擎更准确地识别网页的内容,也有助于第三方工具抓取网站基本信息. ...
- Web前端开发最佳实践系列文章汇总
Web前端开发最佳实践(1):前端开发概述 Web前端开发最佳实践(2):前端代码重构 Web前端开发最佳实践(3):前端代码和资源的压缩与合并 Web前端开发最佳实践(4):在页面中添加必要的met ...
- web前端开发最佳实践笔记
一.文章开篇 由于最近也比较忙,一方面是忙着公司的事情,另外一方面也是忙着看书和学习,所以没有时间来和大家一起分享知识,现在好了,终于回归博客园的大家庭了,今天我打算来分享一下关于<web前端开 ...
- Web前端开发最佳实践(9):CSS代码太太乱,重复代码太多?你需要精简CSS代码
前言 提高网站整体加载速度的一个重要手段就是提高代码文件的网络传输速度.之前提到过,所有的代码文件都应该是经过压缩了的,这可提高网络传输速度,提高性能.除了压缩代码之外,精简代码也是一种减小代码文件大 ...
- Web前端开发最佳实践(8):还没有给CSS样式排序?其实你可以更专业一些
前言 CSS样式排序是指按照一定的规则排列CSS样式属性的定义,排序并不会影响CSS样式的功能和性能,只是让代码看起来更加整洁.CSS代码的逻辑性并不强,一般的开发者写CSS样式也很随意,所以如果不借 ...
- Web前端开发最佳实践(7):使用合理的技术方案来构建小图标
大家都对网站上使用的小图标肯定都不陌生,这些小图标作为网站内容的点缀,增加了网站的美观度,提高了用户体验,可是你有没有看过在这些网站中使用的图标都是用什么技术实现的?虽然大部分网站还是使用普通的图片实 ...
- Web前端开发最佳实践(1):前端开发概述
引言 我从07年开始进入博客园,从最开始阅读别人的文章到自己开始尝试表达一些自己对技术的看法.可以说,博客园是我参与技术讨论的一个主要的平台.在这其间,随着接触技术的广度和深度的增加,也写了一些得到了 ...
- Web前端开发最佳实践(2):前端代码重构
前言 代码重构是业内经常讨论的一个热门话题,重构指的是在不改变代码外部行为的情况下进行源代码修改,所以重构之前需要考虑的是重构后如何才能保证外部行为不改变.对于后端代码来说,可以通过大量的自动化测试来 ...
- Web前端开发最佳实践(5):正确闭合HTML标签,停止使用不标准的标签和属性
正确闭合HTML标签 HTML元素的内容模型定义了元素的结构,表明元素可以包含哪些内容以及元素可以有哪些属性.元素可以包含的内容包括其他元素和字符,但是也有一些元素是空元素,即不能包含任何内容,这些元 ...
随机推荐
- .Net进阶系列(13)-异步多线程(Task和Parallel)(被替换)
一. Task开启多线程的三种形式 1. 利用TaskFactory下的StartNew方法,向StartNew传递无参数的委托,或者是Action<object>委托. 2. 利用Tas ...
- 【转】LR分析法
转自:http://guanjy0129.blog.163.com/blog/static/1115494452010614113333509/ LR分析法的归约过程是规范推导的逆过程,所以LR分析过 ...
- Cmder-控制台模拟器
Cmder是一个软件包,由于在Windows上缺少漂亮的控制台模拟器而纯粹受挫. 它基于令人惊叹的软件,并采用Monokai配色方案和自定义快速布局,从一开始就看起来很性感. 首先,展示一下界面,和W ...
- VSCode 拓展插件推荐
想让VS code干活快起来,插件少不了,开始吧: 快捷键:Ctrl+Shift+X打开插件搜索安装即可 (安装的插件通常会保存在这个目录:C:\Users\你的系统登录用户如administrato ...
- DIV仿textarea文本域,contenteditable如何只能输入纯文本
对于支持HTML5浏览器有2种方法: 1. HTML5 <div contenteditable="plaintext-only"></div> 2. C ...
- (一)求 int 型数据在内存中存储时 1 的个数
题目:求 int 型数据在内存中存储时 1 的个数 描述:输入一个 int 型数据,计算出该 int 型数据在内存中存储时 1 的个数 运行时间限制: 10 sec 内存限制:128 MByte 输入 ...
- 《深入理解java虚拟机》 第七章虚拟机类加载机制
第七章 虚拟机类加载机制 7.1概述 虚拟机把描述类的数据从Class文件加载到内存,并对数据进行检验.转换解析和初始化,最终形成可以被虚拟机直接使用的java类型,这就是虚拟机的类加载机制. 在 ...
- 列表控件QListWidget
列表控件可以让我们以列表形式呈现内容,是界面更加有序美观.QListWidget列表控件应当与QListWidgetItem一起使用,后者作为项被添加入列表控件中,也就是说列表控件中的每一项都是一个Q ...
- <转载>Mac下,使用sshpass让iterm2支持多ssh登录信息保存
windows里有个Xshell非常的方便好使,因为它能保存你所有的ssh登录帐号信息.MAC下并没有xshell,有些也提供这样的功能,但效果都不好.iterm2是很好的终端,但却不能很好的支持多p ...
- 基于json文件实现的gearman任务自动重启
一:在gearman任务失败后,调用task_failed def task_failed(task, *args): info = '\n'.join(args) datetime = local_ ...