一次失败的jQuery优化尝试小结


Posted in Javascript onFebruary 06, 2011

(这并不意味着jQuery的性能是优秀的, 反之只能说它是一个相对封闭的库,无法从外部介入进行优化)。这篇文章就记录一次失败的优化经历。
优化思想
这一次优化的思想来自于数据库。在数据库优化的时候,我们常会说“将大量的操作放在一个事务中一起提交,能有效提高效率”。虽然对数据库不了解的我并不知道其原因,但是“事务”的思想却为我指明了方向(虽然是错的……)。
因此我尝试将“事务”这一概念引入到jQuery中,通过“打开”和“提交”事务,从外部对jQuery进行一些优化,其最重要的在于减少each函数的循环次数。
众所周知,jQuery的DOM操作,以get all, set first为标准,其中用于设置DOM属性/样式的操作,几乎都是对选择出来的元素的一次遍历,jQuery.access函数就是其中最核心的部分,其中用于循环的代码如下:

// Setting one attribute 
if ( value !== undefined ) { 
// Optionally, function values get executed if exec is true 
exec = !pass && exec && jQuery.isFunction(value); 
for ( var i = 0; i < length; i++ ) { 
fn( 
elems[i], 
key, 
exec ? value.call(elems[i], i, fn(elems[i], key)) : value, 
pass 
); 
} 
return elems; 
}

比如jQuery.fn.css函数就是这样的:
jQuery.fn.css = function( name, value ) { 
// Setting 'undefined' is a no-op 
if ( arguments.length === 2 && value === undefined ) { 
return this; 
} 
return jQuery.access( this, name, value, true, function( elem, name, value ) { 
return value !== undefined ? 
jQuery.style( elem, name, value ) : 
jQuery.css( elem, name ); 
}); 
};

因此,下面这样的代码,假设被选择的div元素有5000个,则要循环访问10000个节点:
jQuery('div').css('height', 300).css('width', 200);
而在我的想法中,在一个“事务”中,可以如数据库的操作一般,通过保存所有的操作,在“提交事务”的时候统一进行,将10000次节点访问,减少至5000次,相当于提升了“1倍”的性能。
简单实现
“事务”式的jQuery操作中,提供2个函数:
begin:打开一个“事务”,返回一个事务的对象。该对象拥有jQuery的所有函数,但是调用函数并不会立刻生效,只有在“提交事务”后才会生效。
commit:提交一个“事务”,保证所有事先调用过的函数都生效,交返回原本的jQuery对象。
实现起来也很方便:
创建一个“事务对象”,复制jQuery.fn上所有函数到该对象中。
当调用某个函数时,在预先准备好的“队列”中添加调用的函数名和相关参数。
当提交事务时,对被选择的元素进行一次遍历,对遍历中的每个节点应用“队列”中的所有函数。
简单地代码如下:
var slice = Array.prototype.slice; 
jQuery.fn.begin = function() { 
var proxy = { 
_core: c, 
_queue: [] 
}, 
key, 
func; 
//复制jQuery.fn上的函数 
for (key in jQuery.fn) { 
func = jQuery.fn[key]; 
if (typeof func == 'function') { 
//这里会因为for循环产生key始终是最后一个循环值的问题 
//因此必须使用一个闭包保证key的有效性(LIFT效应) 
(function(key) { 
proxy[key] = function() { 
//将函数调用放到队列中 
this._queue.push([key, slice.call(arguments, 0)]); 
return this; 
}; 
})(key); 
} 
} 
//避免commit函数也被拦截 
proxy.commit = jQuery.fn.commit; 
return proxy; 
}; 
jQuery.fn.commit = function() { 
var core = this._core, 
queue = this._queue; 
//仅一个each循环 
core.each(function() { 
var i = 0, 
item, 
jq = jQuery(this); 
//调用所有函数 
for (; item = queue[i]; i++) { 
jq[item[0]].apply(jq, item[1]); 
} 
}); 
return this.c; 
};

测试环境
测试使用以下条件:
5000个div放在一个容器(<div id="container"></div>)中。
使用$('#container>div')选择这5000个div。
每个div要求设置一个随机背景色(randomColor函数),和800px以下的随机宽度(randomWidth函数)。
参加测试的调用方法有3个:
正常使用法:
$('#container>div') 
.css('background-color', randomColor) 
.css('width', randomWidth);

单次循环法:
$('#container>div').each(function() { 
$(this).css('background-color', randomColor).css('width', randomWidth); 
});

事务法:
$('#container>div') 
.begin() 
.css('background-color', randomColor) 
.css('width', randomWidth) 
.commit();

对象赋值法:
$('#container>div').css({ 
'background-color': randomColor, 
'width': randomWidth 
});

测试浏览器选择Chrome 8系列(用IE测就直接挂了)。
悲伤的结果
原本的预测结果是,单次循环法的效率远高于正常使用法,同时事务法虽然比单次循环法慢一些,但应该比正常使用法更快,而对象赋值法其实是jQuery内部支持的单次循环法,效率应该是最高的。
然而遗憾的是,结果如下:
正常使用法 单次循环法 事务法 对象赋值法
18435ms 18233ms 18918ms 17748ms
从结果上看,事务法成了最慢的一种方法。同时单次循环与正常使用并没有明显的优势,甚至依赖jQuery内部实现的对象赋值法也没有拉开很大的差距。
由于5000个元素的操作已经是非常庞大的循环,如此庞大的循环也没能拉开性能的差距,平时最常用的10个左右的元素操作更不可能有明显的优势,甚至还可能将劣势扩大化。
究其原因,由于本身单次循环法就没有明显的性能提升,因此依赖于单次循环,并是在单次循环之上进行外部构建的事务法,自然是在单次循环的基础上还需要额外增加创建事务对象、保存函数队列、遍历函数队列等开销,结果败给正常使用法也在情理之中。
至此,也算是可以宣布模仿“事务”的优化之道的失败。但是对这个结果却还能进一步地分析。
性能在哪里
首先,从代码的使用上来分析,将正常使用法和测试中最快的对象赋值法进行比较,可以说两者的差距仅在于循环的元素个数的不同(这里抛开了jQuery的内部问题,事实上jQuery.access的糟糕实现也确实有拖对象赋值法后腿之嫌,好在并不严重),正常使用法是10000个元素,对象赋值法是5000个元素。因此可以简单地认为,18435 - 17748 = 687ms是循环5000个元素的耗时,这占到整个执行过程的3.5%左右,并不是整个执行过程的主干,其实真的没有优化的必要。

那么另外96.5%的开销去了哪里呢?谨记Doglas的一句话,“事实上Javascript并不慢,慢的是DOM的操作”。其实剩下96.5%的开销中,除去函数调用等基本的消耗,至少有95%的时间是用在了DOM元素的样式被改变后的重新渲染之上。

发现了这个事实之后,其实也就有了更正确的优化方向,也是前端性能中的基本原则之一:在修改大量子元素时,先将根父DOM节点移出DOM树。因此如果使用以下的代码再进行测试:

//没有重用$('#container')已经很糟糕了 
$('#container').detach().find('div') 
.css('background-color', randomColor) 
.css('width', randomWidth); 
$('#container').appendTo(document.body);

测试结果始终停留在900ms左右,与前面的数据完全不在一个数量级之上,真正的优化成功。

教训和总结
千万要找到正确的性能瓶颈再进行优化,盲目的猜测只会导致走上错误而偏激的道路。
数据说话,数据面前谁也别说话!
不认为“事务”这个方向是错误的,如果jQuery原生就能支持“事务”这样的概念,会不会有其他的点可以优化?比如一个事务自动会将父元素脱离出DOM树之类的……

Javascript 相关文章推荐
用javascript实现的支持lrc歌词的播放器
May 17 Javascript
jquery常用技巧及常用方法列表集合
Apr 06 Javascript
jQuery-1.9.1源码分析系列(十)事件系统之事件体系结构
Nov 19 Javascript
使用Promise解决多层异步调用的简单学习心得
May 17 Javascript
详解利用 Vue.js 实现前后端分离的RBAC角色权限管理
Sep 15 Javascript
使用axios实现上传图片进度条功能
Dec 21 Javascript
微信小程序自定义底部弹出框
Nov 16 Javascript
vue-resource请求实现http登录拦截或者路由拦截的方法
Jul 11 Javascript
JSON生成Form表单的方法示例
Nov 21 Javascript
vue项目中使用fetch的实现方法
Apr 25 Javascript
让你30分钟快速掌握vue3教程
Oct 26 Javascript
开发一个封装iframe的vue组件
Mar 29 Vue.js
DOM_window对象属性之--clipboardData对象操作代码
Feb 03 #Javascript
基于jQuery的自动完成插件
Feb 03 #Javascript
jQuery初学:find()方法及children方法的区别分析
Jan 31 #Javascript
javascript event 事件解析
Jan 31 #Javascript
javascript getElementsByTagName
Jan 31 #Javascript
js对象数组按属性快速排序
Jan 31 #Javascript
javascript 节点排序 2
Jan 31 #Javascript
You might like
CodeIgniter启用缓存和清除缓存的方法
2014/06/12 PHP
WordPress过滤垃圾评论的几种主要方法小结
2016/07/11 PHP
PHP转换文本框内容为HTML格式的方法
2016/07/20 PHP
php封装的page分页类完整实例
2016/10/18 PHP
Thinkphp5.0 框架使用模型Model添加、更新、删除数据操作详解
2019/10/11 PHP
javascript 字符 Escape,encodeURI,encodeURIComponent
2009/07/09 Javascript
javascript数组去掉重复
2011/05/12 Javascript
JS中判断null、undefined与NaN的方法
2014/03/24 Javascript
js QQ客服悬浮效果实现代码
2014/12/12 Javascript
jQuery插件passwordStrength密码强度指标详解
2016/06/24 Javascript
js从外部获取图片的实现方法
2016/08/05 Javascript
JS实现密码框的显示密码和隐藏密码功能示例
2016/12/26 Javascript
原生js实现水平方向无缝滚动
2017/01/10 Javascript
vue.js树形组件之删除双击增加分支实例代码
2017/02/28 Javascript
JavaScript组件开发之输入框加候选框
2017/03/10 Javascript
node实现的爬虫功能示例
2018/05/04 Javascript
分享一个vue项目“脚手架”项目的实现步骤
2019/05/26 Javascript
如何阻止小程序遮罩层下方图层滚动
2019/09/05 Javascript
layui 地区三级联动 form select 渲染的实例
2019/09/27 Javascript
Vue自定义表单内容检查rules实例
2020/10/30 Javascript
JQuery+drag.js上传图片并且实现图片拖曳
2020/11/18 jQuery
JavaScript构造函数原理及实现流程解析
2020/11/19 Javascript
在Python中操作列表之List.pop()方法的使用
2015/05/21 Python
python3 字符串/列表/元组(str/list/tuple)相互转换方法及join()函数的使用
2019/04/03 Python
Python多版本开发环境管理工具介绍
2019/07/03 Python
python 图像判断,清晰度(明暗),彩色与黑白实例
2020/06/04 Python
Python通过Schema实现数据验证方式
2020/11/12 Python
python 爬取百度文库并下载(免费文章限定)
2020/12/04 Python
一张图片能隐含千言万语之隐藏你的程序代码
2012/12/13 HTML / CSS
第一范式(1NF)、第二范式(2NF)和第三范式(3NF)之间的区别是什么?
2016/04/28 面试题
学校出纳员岗位职责
2014/03/18 职场文书
民族团结好少年事迹材料
2014/08/19 职场文书
2015高三毕业寄语赠言
2015/02/27 职场文书
酒店工程部的岗位职责汇总大全
2019/10/23 职场文书
mysql查询的控制语句图文详解
2021/04/11 MySQL
Django项目如何正确配置日志(logging)
2021/04/29 Python