今天有个需求需要在浏览器端处理大量数据, 但怕浏览器在处理数据的同时, 浏览器卡主或提示脚本运行过慢, 对客户造成不好的体验. 需要优化处理 js.
在 web 开发的时候经常会遇到浏览器不响应事件进入假死状态, 甚至弹出 "脚本运行时间过长" 的提示框, 如果出现这种情况说明你的脚本已经失控了, 必须进行优化.
为什么会出现这种情况呢, 我们先来看一下浏览器的内核处理方式:
浏览器的内核是多线程的, 它们在内核制控下相互配合以保持同步, 一个浏览器至少实现三个常驻线程: javascript 引擎线程, GUI 渲染线程, 浏览器事件触发线程.
JavaScript 引擎是基于事件驱动单线程执行的, JS 引擎一直等待着任务队列中任务的到来然后加以处理, 浏览器无论再什么时候都只有一个 JS 线程在运行 JS 程序.
GUI 渲染线程负责渲染浏览器界面, 当界面需要重绘 (Repaint) 或由于某种操作引发回流 (reflow) 时, 该线程就会执行. 但需要注意 GUI 渲染线程与 JS 引擎是互斥的, 当 JS 引擎执行时 GUI 线程会被挂起, GUI 更新会被保存在一个队列中等到 JS 引擎空闲时立即被执行.
事件触发线程, 当一个事件被触发时该线程会把事件添加到待处理队列的队尾, 等待 JS 引擎的处理. 这些事件可来自 JavaScript 引擎当前执行的代码块如 setTimeOut, 也可来自浏览器内核的其他线程如鼠标点击, AJAX 异步请求等, 但由于 JS 的单线程关系所有这些事件都得排队等待 JS 引擎处理.
了解了浏览器的内核处理方式就不难理解浏览器为什么会进入假死状态了, 当一段 JS 脚本长时间占用着处理机就会挂起浏览器的 GUI 更新, 而后面的事件响应也被排在队列中得不到处理, 从而造成了浏览器被锁定进入假死状态. 另外 JS 脚本中进行了 DOM 操作, 一旦 JS 调用结束就会马上进行一次 GUI 渲染, 然后才开始执行下一个任务, 所以 JS 中大量的 DOM 操作也会导致事件响应缓慢甚至真正卡死浏览器, 如在 IE6 下一次插入大量的 html. 而如果真的弹出了 "脚本运行时间过长" 的提示框则说明你的 JS 脚本肯定有死循环或者进行过深的递归操作了.
Nicholas C. Zakas 认为不论什么脚本, 在任何时间, 任何浏览器上执行都不应该超过 100 毫秒, 否则一定要将脚本分解成若干更小的代码段. 那么我们该如何来做呢:
第一步, 优化你的循环, 循环体中包含太多的操作和循环的次数过多都会导致循环执行时间过长, 并直接导致锁死浏览器. 如果循环之后没有其他操作, 每次循环只处理一个数值, 而且不依赖于上一次循环的结果则可以对循环进行拆解, 看下面的 chunk 的函数:
- function chunk(array, process, context) {
- setTimeout(function() {
- var item = array.shift();
- process.call(context, item);
- if (array.length > 0) {
- setTimeout(arguments.callee, 100);
- }), 100);
- }
chunk()函数的用途就是将一个数组分成小块处理, 它接受三个参数: 要处理的数组, 处理函数以及可选的上下文环境. 每次函数都会将数组中第一个对象取出交给 process 函数处理, 如果数组中还有对象没有被处理则启动下一个 timer, 直到数组处理完. 这样可保证脚本不会长时间占用处理机, 使浏览器出一个高响应的流畅状态.
其实在我看来, 借助 JS 强大的闭包机制任何循环都是可拆分的, 下面的版本增加了 callback 机制, 使可再循环处理完毕之后进行其他的操作.
- function chunk(array,process,cbfun){
- var i=0,len = array.length; // 这里要注意在执行过程中数组最好是不变的
- setTimeout(function(){
- process( array[i] , i++ ); // 循环体要做的操作
- if( i < len ){
- setTimeout(arguments.callee,100)
- }else{
- cbfun() // 循环结束之后要做的操作
- }
- }
- }
第二步, 优化你的函数, 如果函数体内有太多不相干但又要一起执行的操作则可以进行拆分, 考虑下面的函数:
- function dosomething(){
- dosomething1();
- dosomething2();
- }
dosomething1 和 dosomething2 互不相干, 执行没有先后次序, 可用前面提到的 chunk 函数进行拆分:
- function dosomething(){
- chunk([dosomething1,dosomething2],function(item){item();})
- }
或者直接交给浏览器去调度
- function dosome(){
- setTimeout(dosomething1,0);
- setTimeout(dosomething2,0);
- }
第三步, 优化递归操作, 函数递归虽然简单直接但是过深的递归操作不但影响性能而且稍不注意就会导致浏览器弹出脚本失控对话框, 必须小心处理.
看以下斐波那契数列的递归算法:
- function fibonacci(n) {
- return n < 2 ? n: fibonacci(n - 1) + fibonacci(n - 2);
- };
fibonacci(40)这条语句将重复调用自身 331160280 次, 在浏览器中执行必然导致脚本失控, 而采用下面的算法则只需要调用 40 次
- fibonacci = function(n){
- var memo = {0:0,1:0}; // 计算结果缓存
- var shell = function(n){
- var result = memo[n];
- if( typeof result != 'number' ) // 如果值没有被计算则进行计算
- memo[n] = shell(n-1) + shell(n -2)
- return memo[n];
- }
- return shell(n);
- }
这项技术被称为 memoization, 他的原理很简单就是同样的结果你没必要计算两次. 另一种消除递归的办法就是利用迭代, 递归和迭代经常会被作为互相弥补的方法.
第四步, 减少 DOM 操作, DOM 操作的代价是相当昂贵的, 大多数 DOM 操作都会触发浏览器的回流 (reflow) 操作. 例如添加删除节点, 修改元素样式, 获取需要经过计算的元素样式等. 我们要做的就是尽量少的触发回流操作.
el.style.width = '300px' el.style.height = '300px' el.style.backgroundColor = 'red'
上面的操作会触发浏览器的三次回流操作, 再看下面的方式:
el.className = 'newStyle'
通过设置改元素的 className 一次设置多个样式属性, 将样式写再 CSS 文件中, 只触发一次回流, 达到了同样是效果而且效率更高. 因为浏览器最擅长的就是根据 class 设置样式.
还有很多可以减少 DOM 操作的方法, 在此就不多说了, 但是一个基本的原则就是让浏览器去做它自己擅长的事情, 例如通过 class 来改变元素的属性.
相信经过上面的优化的过程必定可以大大提高用户体验, 不会出现浏览器被锁死和弹出脚本失控的对话框, 使你的浏览器从繁重的任务中解放出来. 需要指出的是上面这些优化并不是必须的, 只有当一段脚本的执行时间真的影响到了用户体验才需要进行. 虽然它们让用户觉得脚本的执行变快了, 但其实完成同一个操作的时间可能被延长了, 这些技术只是让浏览器处于一个快速响应的状态, 使用户浏览更流畅.
最后送一句忠告: 过早优化是万恶之源.
尊重版权, 看人家总结的挺好: 优化 js 脚本设计, 防止浏览器假死 http://www.nowamagic.net/librarys/veda/detail/787
第一步, 优化你的循环, 循环体中包含太多的操作和循环的次数过多都会导致循环执行时间过长, 并直接导致锁死浏览器. 如果循环之后没有其他操作, 每次循环只处理一个数值, 而且不依赖于上一次循环的结果则可以对循环进行拆解, 看下面的 chunk 的函数:
- http://www.cnblogs.com/fumj/admin/
- function chunk(array, process, context){
- setTimeout(function(){
- var item = array.shift();
- process.call(context, item);
- if(array.length >0){
- setTimeout(arguments.callee,100);
- }),100);
- }
- <OBJECT id=ZeroClipboardMovie_1 data="http://www.dewen.org/theme/images/editor/ZeroClipboard.swf" width=16 align=middle height=15 type=application/x-shockwave-flash bgcolor="#ffffff">
chunk()函数的用途就是将一个数组分成小块处理, 它接受三个参数: 要处理的数组, 处理函数以及可选的上下文环境. 每次函数都会将数组中第一个对象取出交给 process 函数处理, 如果数组中还有对象没有被处理则启动下一个 timer, 直到数组处理完. 这样可保证脚本不会长时间占用处理机, 使浏览器出一个高响应的流畅状态.
其实在我看来, 借助 JS 强大的闭包机制任何循环都是可拆分的, 下面的版本增加了 callback 机制, 使可再循环处理完毕之后进行其他的操作.
- http://www.cnblogs.com/fumj/admin/
- function chunk(array,process,cbfun){
- var i=0,len = array.length; // 这里要注意在执行过程中数组最好是不变的
- setTimeout(function(){
- process( array[i], i++); // 循环体要做的操作
- if( i < len ){
- setTimeout(arguments.callee,100)
- }else{
- cbfun() // 循环结束之后要做的操作
- }
- }
- }
- <OBJECT id=ZeroClipboardMovie_2 data="http://www.dewen.org/theme/images/editor/ZeroClipboard.swf" width=16 align=middle height=15 type=application/x-shockwave-flash bgcolor="#ffffff">
第二步, 优化你的函数, 如果函数体内有太多不相干但又要一起执行的操作则可以进行拆分, 考虑下面的函数:
- http://www.cnblogs.com/fumj/admin/
- function dosomething(){
- dosomething1();
- dosomething2();
- }
- <OBJECT id=ZeroClipboardMovie_3 data="http://www.dewen.org/theme/images/editor/ZeroClipboard.swf" width=16 align=middle height=15 type=application/x-shockwave-flash bgcolor="#ffffff">
dosomething1 和 dosomething2 互不相干, 执行没有先后次序, 可用前面提到的 chunk 函数进行拆分:
- http://www.cnblogs.com/fumj/admin/
- function dosomething(){
- chunk([dosomething1,dosomething2],function(item){item();})
- }
- <OBJECT id=ZeroClipboardMovie_4 data="http://www.dewen.org/theme/images/editor/ZeroClipboard.swf" width=16 align=middle height=15 type=application/x-shockwave-flash bgcolor="#ffffff">
或者直接交给浏览器去调度
- http://www.cnblogs.com/fumj/admin/
- function dosome(){
- setTimeout(dosomething1,0);
- setTimeout(dosomething2,0);
- }
- <OBJECT id=ZeroClipboardMovie_5 data="http://www.dewen.org/theme/images/editor/ZeroClipboard.swf" width=16 align=middle height=15 type=application/x-shockwave-flash bgcolor="#ffffff">
第三步, 优化递归操作, 函数递归虽然简单直接但是过深的递归操作不但影响性能而且稍不注意就会导致浏览器弹出脚本失控对话框, 必须小心处理.
看以下斐波那契数列的递归算法:
- http://www.cnblogs.com/fumj/admin/
- function fibonacci(n){
- return n <2? n: fibonacci(n -1)+ fibonacci(n -2);
- };
- <OBJECT id=ZeroClipboardMovie_6 data="http://www.dewen.org/theme/images/editor/ZeroClipboard.swf" width=16 align=middle height=15 type=application/x-shockwave-flash bgcolor="#ffffff">
fibonacci(40)这条语句将重复调用自身 331160280 次, 在浏览器中执行必然导致脚本失控, 而采用下面的算法则只需要调用 40 次
- http://www.cnblogs.com/fumj/admin/
- fibonacci =function(n){
- var memo ={0:0,1:0}; // 计算结果缓存
- var shell =function(n){
- var result = memo[n];
- if(typeof result !='number') // 如果值没有被计算则进行计算
- memo[n]= shell(n-1)+ shell(n -2)
- return memo[n];
- }
- return shell(n);
- }
- <OBJECT id=ZeroClipboardMovie_7 data="http://www.dewen.org/theme/images/editor/ZeroClipboard.swf" width=16 align=middle height=15 type=application/x-shockwave-flash bgcolor="#ffffff">
这项技术被称为 memoization, 他的原理很简单就是同样的结果你没必要计算两次. 另一种消除递归的办法就是利用迭代, 递归和迭代经常会被作为互相弥补的方法.
第四步, 减少 DOM 操作, DOM 操作的代价是相当昂贵的, 大多数 DOM 操作都会触发浏览器的回流 (reflow) 操作. 例如添加删除节点, 修改元素样式, 获取需要经过计算的元素样式等. 我们要做的就是尽量少的触发回流操作.
- http://www.cnblogs.com/fumj/admin/
- el.style.width ='300px' el.style.height ='300px' el.style.backgroundColor ='red'
- <OBJECT id=ZeroClipboardMovie_8 data="http://www.dewen.org/theme/images/editor/ZeroClipboard.swf" width=16 align=middle height=15 type=application/x-shockwave-flash bgcolor="#ffffff">
上面的操作会触发浏览器的三次回流操作, 再看下面的方式:
- http://www.cnblogs.com/fumj/admin/
- el.className ='newStyle'
- <OBJECT id=ZeroClipboardMovie_9 data="http://www.dewen.org/theme/images/editor/ZeroClipboard.swf" width=16 align=middle height=15 type=application/x-shockwave-flash bgcolor="#ffffff">
通过设置改元素的 className 一次设置多个样式属性, 将样式写再 CSS 文件中, 只触发一次回流, 达到了同样是效果而且效率更高. 因为浏览器最擅长的就是根据 class 设置样式.
还有很多可以减少 DOM 操作的方法, 在此就不多说了, 但是一个基本的原则就是让浏览器去做它自己擅长的事情, 例如通过 class 来改变元素的属性.
相信经过上面的优化的过程必定可以大大提高用户体验, 不会出现浏览器被锁死和弹出脚本失控的对话框, 使你的浏览器从繁重的任务中解放出来. 需要指出的是上面这些优化并不是必须的, 只有当一段脚本的执行时间真的影响到了用户体验才需要进行. 虽然它们让用户觉得脚本的执行变快了, 但其实完成同一个操作的时间可能被延长了, 这些技术只是让浏览器处于一个快速响应的状态, 使用户浏览更流畅.
来源: https://my.oschina.net/leamon/blog/597944