高性能js

182 阅读14分钟

缺陷

这本书是2010年出版的,这本书谈性能是有时效性的,这几年前端发展的速度是飞快的,书里面还有一些内容考虑IE6、7、8的东西,殊不知现在这些都已经不再考虑了,所以不可避免的有一些知识是比较老的。

章节1、加载和执行

第一条定律:将脚本放在底部。

大家都知道,浏览器在遇到<body>标签之前,不会渲染页面中的任何部分,如果我们把<script>标签全部放在<body>标签之前,
大家想想界面是不是一片空白,直至浏览器将所有js都下载并执行后才会去渲染页面,
这也就是为什么要将所有<script>标签放在尽可能接近<body>标签底部也就是</body>前的原因。

第二条定律:将脚本成组打包。

合并脚本。页面中的

在此特意说明一下
1、为什么很多人特别喜欢用外部文件来写js而不用内联js,
这是因为1)外部文件易于管理2)浏览器有缓存外部文件的功能,同样一个js文件如果被多个界面共享使用,
第二个使用的可以直接从缓存中拿而不用从远程服务器上去下载,省去了好多时间及流量呀

2、为什么许多人会用CDNContent Delivery Network)来加载公用js比如说jquery.js,
这是因为从别的网站比如说jQuery官网下载jQuery占有的是别人服务器的资源,减轻了本地服务器的负载,节省网站分布式架构的支出成本和运维成本。

3、上文中讲了将多个js文件打包成一个js文件,可以提高性能,可以试试用Yahoo! Combo handler ,这篇文章介绍了一下http://www.ooso.net/archives/458  
总归一句话就是加载js的时候会阻塞页面渲染,那怎么样让脚本不阻塞页面渲染呢,唯一的方法就是在window.onload发生后开始下载js

第三条定律:使用非阻塞方式下载js

第一种方法:延迟的脚本

为script增加了一个defer属性,是指当前script脚本不打算修改DOM,所以代码可以稍后执行。
如:<script type="file1.js" defer></script>

1) defer并不是所有浏览器都支持的; 当defer指定的脚本文件被下载时,它不会阻塞浏览器的其它处理过程;
2) defer指定的脚本文件是可以与页面中的其它资源并行下载的;任何被指定为defer的脚本文件一定会在DOM加载完成之后才会被执行,也就是在onload事件之前被执行。

第二种方法:动态创建脚本

通过js创建script标签指定它的src来加载脚本文件,这种方法使得脚本下载和运行不会阻塞其它页面处理程序,

所以你甚至可以把这种动态创建脚本的文件放在head标签当中都没有关系。

<script>
    var script = document.createElement('script');
    script.type = 'text/javascript';
    script.src = "file1.js";
    document.getElementsByTagName('head')[0].appendChild(script);
</script>

fifrfox,opera,chorme和safri会在

IE支持另一种实现方式,它会触发readystatechange事件

第三种方法:XHR脚本注入

这种方法的主要优点是你可以下载不立即执行的脚本文件,但是缺点就是你要动态加载的js文件必须与当前运行的页面在同一个域中。

<script>
        var xhr = new XMLHttpRequest();
        xhr.open("get","common.js",true);
        xhr.onreadystatechange = function(){
            if(xhr.readyState==4){
                if(xhr.status>=200&&xhr.status<300||xhr.status == 304){
                    var script = document.createElement("script");
                    script.type="text/javascript";
                    script.text = xhr.responseText;
                    document.body.appendChild(script);
                }
            }
        };
        xhr.send(null);
</script>

下面介绍几个动态加载的脚本库

(1)LazyLoad--可以下载多个脚本文件并保证在所有浏览器上按顺序执行。

(虽然可以动态加载多个文件,但还是建议尽可能的减少文件数量,因为每个下载仍然是一个单独的HTTP请求)

<script src="lazyload.min.js"></script>
<script>
    LazyLoad.js("common.js",function(){
        Application.init();
    })
</script>

如是多个文件时,

LazyLoad.js(["file1.js","file2.js"],function(){
        Application.init();
})
附:AngularJs 通过 ocLazyLoad 实现动态(懒)加载模块和依赖
http://www.jianshu.com/p/4a66c578870b
http://blog.pkcms.cn/angularjs-oclazyload/

(2)Labs.js 它的script方法用于加载js文件,wait方法用于等待文件下载并运行之后执行的回调函数,

通过wait方法允许你哪些文件应该等待其它文件,具体用法可以参照labs使用方法

LAB.script()方法用来定义需要下载的js文件,LAB.script()方法用来定义需要下载的js文件,LAB.wait()用来指定文件下载并执行完毕后所需调用的函数,鼓励链式操作。

<script src="lab.js"></script>
<script>
    $LAB.script("file1.js")
            .script("file2.js")
            .wait(function(){
                Application.init();
            })
</script>

章节2、数据存取

1、数据类型

对于任何一种编程语言来说,数据存储的位置关系到访问速度!

在JavaScript中
直接量----包括字符串string、数字number、布尔值boolean、对象object、数组array、函数function、
          正则表达式regular expression、空值null、未定义数组undefined。
数组项----则需要通过数组的数字索引来访问,
对象-----通过字符串进行索引来访问其成员

总结:直接量和局部变量的访问速度要远已于数组项和对象成员,所以我们应该尽量使用直接量和局部变量,尽量限制使用数组项和对象成员。

2、管理作用域

js中每个函数事实上就是对象,每个函数内部都会有一个Scope属性,
这个属性包含被创建的作用域中对象的集合,所以事实上内部Scope属性就是函数的作用域。

在函数运行时,会建立一个运行期上下文(运行期上下文事实上就是函数运行时的环境,
对于函数的每次运行而言,运行期上下文都是独一的,函数执行完毕运行期上下文将被销毁,
也就是说多次运行同一个函数就会创建多个运行期上下文)。

当函数运行期上下文确定后,它的作用域将被初始化(作用域初始化包括函数参数、函数内部变量、this),
作用域初始化之后将被作为激活对象推入作用域链的前端。(事实上这就是原型链的原理,
js就是通过原型链来实现继承,通过闭包来实现成员的公有和私有),
这也是为什么访问全局变量是最慢的,因为全局变量它是处于运行期上下文作用域链的最后一个位置。

章节3、DOM编程

所以事实上DOM和BOM是两个独立的部分,它们之间的通信是通过相互之间的功能接口来实现的,这样说来两个独立的部分以功能接口必定会带来性能损耗。 这也就是为什么大家一致都说尽量少去访问和修改DOM元素。

在JS高级程序设计第一章当中就把JavaScript分成三大部分 ECMAScript,提供核心语言功能 文档对象模型DOM,提供访问和操作网页内容的方法和接口 浏览器对象模型BOM,提供与浏览器交互的方法和接口

优化1、最小化DOM访问次数,尽可能在js端处理

优化2、如果需要多次访问某个DOM节点,请使用局部变量存储它的引用

<script>
    function innerHTMLLoop2(){
        var content ="";
        for(var count=0; count<15000;count++){
            content += 'a';
        }
        document.getElementById('here').innerHTML += content;
    }
</script>

优化3、小心处理HTML集合,因为它实时连系着底层文档,把集合的长度缓存到一个变量中,并在迭代中使用它。

如果需要经常操作集合,建议把它拷贝到一个数组中。

html集合指的是DOM方法返回的数组对象
document.getElementsByName();
document.getElementsByClassName();
document.getElementsByTagName();
以及下面这些
document.images; 所有图片元素
document.links; 所有a标签元素
document.forms;
document.forms[0].elements;
总结一句话就是说HTML集合事实上是虚拟存在的,意味着当底层文档更新时,它们将自动更新。
html集合一直与文档保持着连接,每次你需要最新信息时,都会重复执行查询的过程,哪怕是获取集合的元素个数,这正是低效之源。
1) 修改DOM元素的时候,尽量使用innerHTML,而不是CreateElement()
2) 克隆元素时,使用element.cloneNode来代替document.createElement()
3) 元素节点,childNodes是一个集合。
这也是为什么很多提高JavaScript性能的建议当中有这样一条了“使用firstChild和nextSibling代替childNodes遍历dom元素” 。
(不信你可以测试一下,明显NextSibling的速度比ChildNodes的快特别是当数据量大的时候一眼就能看出来)

优化4、使用速度更快的API,如querySelectorAll()和firstElementChild

选择器API的querySelectorAll()方法是使用css选择器作为参数并返回一个静态列表,而不是html集合。
如1: var elements = document.querySelectorAll('#menu a');     //静态列表
          var elements = document.getElementById('menu').getElementsByTagName('a');      //html集合

如2:获取warning与notice的所有元素。
       var errs = document.querySelectorAll('div.warning, div.notice');
        等价于:
        var errs = [];
        var divs = document.getElementsByTagName('div');
        for(var i= 0,len=div.length; i<len; i++){
            className = divs[i].className;
            if(className==='warning'||className ==='notice'){
                errs.push(divs[i]);
            }
        }

优化5、留意重绘和重排,批量修改样式时,“离线”操作DOM树,使用缓存,并减少访问布局信息的次数。

当DOM变化影响到元素的几何属性时,浏览器就需要重新计算元素的几何属性,同样其它元素的几何属性和位置也会因此受到影响。
浏览器会使渲染树中受到影响的部分失控,并重新构造渲染树,这个过程叫重排,
完成重排后,浏览器会重新绘制受影响的部分到屏幕中,该过程叫重绘

通过以下步骤来减少重排与重绘的次数
1、从文档流中摘下此元素
2、对其应用多重改变
3、将元素带回文档中

有三种方法可以使dom脱离文档
1、隐藏元素,对其进行修改后再显示
2、使用文档片断在已存DOM之外创建一个子树,然后将它拷贝到文档中(推荐使用此方法,因为它是涉及最小数量的DOM操作和重排)
3、将原始元素拷贝到一个脱离文档的节点中,修改副本,完成后然后覆盖原始元素

优化6、动画中使用绝对定位,使用拖放代理

优化7、使用事件委托来减少事件处理器的数量

章节4、算法和流程控制

在js的四种循环(for、for-in、while、do-while),只有for-in循环比其它几种明显要慢,另外三种速度区别不大 有一点需要注意的是,javascript没有块级作用域,只有函数级作用域,也就是说在for循环初始化中的var语句会创建一个函数级变量而非循环级变量

优化循环的方法有如下:
1、减少对象成员及数组项的查找次数(使用局部变量保存需要查找的对象成员)
2、颠倒数组的顺序来提高循环性能,也就是从最后一项开始向前处理
3、使用for循环而非jQuery的each来遍历数组,
因为jQuery的each方法是基于函数的迭代。尽管基于函数的迭代提供了一个更为便利的迭代方法,
但它比基于循环的迭代在慢许多。

4、在大多数情况下switchif-else运行得要快,所以多个时,switch语句是更佳的选择
5、优化if-else最简单的方法就是确保最可能出现的条件放在首位,另外一个方法就是优化条件判断的次数
6、使用递归虽然可以把复杂的算法变得简单,但递归函数如果终止条件不明确或缺少终止条件会导致函数长时间运行。
所以递归函数还可能会遇到浏览器“调用栈大小限制”

使用优化后的循环来替代长时间运行的递归函数可以提升性能,因为运行一个循环比反复调用一个函数的开销要少的多
如果循环资料太多,可以考虑使用如下介绍的达夫设备原理来提升性能

章节5、字符串和正则表达式小结:

1.字符串合并的时候使用简单的'+'和'+='操作符。

str+='abc'+'efg;//2个以上的字符串拼接,会产生临时字符串
str=str+'abc'+'efg';//推荐,提速10%~40%  

2.书里面讲的正则原理和回溯原理,这个很重要,找个篇博客:跟书里面讲的差不多,但还是建议大家可以去找找PDF好好看看正则表达式这节。

3.提高正则表达式效率的方法:

 1、最重要的是:具体化正则表达式!具体化正则表达式!具体化正则表达式!
 2、关注如何让匹配更快失败,找出一些必需,特殊的字符
 3、正则表达式以简单的、必需的字元开始。
 4、使用量词模式,使它们后面的字元互斥。
 5、较少分支数量,缩小分支范围
 6、使用合适的量词
 7、把正则表达式赋值给变量并重用
 8、将复杂的正则拆分为简单的片段
 //事实上,书里面讲的方法不止这么几个,而且每一个都有详细的解析 大佬们 还是去看看这一章节吧

4.正则表达式并不总是最好的解决方案,当你只是搜索字面字符串或者你事先知道字符串的哪一部分将要被查找时:

 使用indexOf()和lastIndexOf()更适合查找特定字符串的位置或者判断它们是否存在
 //例如:判断当前浏览器之类。

章节6、快速响应的用户界面小结:

js和用户界面更新在同一个进程中运行,因此一次只能处理一件事情。高效的管理UI线程就是要确保js不能运行太长时间,以免影响用户体验。

1.浏览器限制了js任务的运行时间,这种限制很有必要,它确保某些恶意代码不能通过永不停止的密集操作锁住用户的浏览器。此限制分为两种:调用栈的大小和长时间运行脚本。

2.任何js任务都不应当执行超过100毫秒。过长的运行时间会导致UI更新出现明显延迟,从而对用户体验造成负面影响。

3.定时器可用来安排代码延迟执行,它使得你可以把长时间运行脚本分解成一系列的小任务。

章节7 ajax

这一章节貌似东西都比较老一点。。

1.post更适合发送大量数据到服务器。

2.get请求能够被浏览器缓存,Expires头信息设置浏览器缓存请求多久。可用于从不改变的图片或者其他静态数据集(js、css等)

3.JSON是一种使用js对象和数组直接量编写的轻量级且易于解析的数据格式,它不仅传输尺寸小,而且解析速度快。JSON是高性能AJAX的基础,尤其在使用动态脚本注入时。

json应该是近几年一直在用的。。。

4.减少请求数,通过合并js和css文件。

5.缩短页面的加载时间,页面主要内容加载完成后,用AJAX获取那些次要的文件。


章节8 编程实践小结

1.避免双重求值:避免使用eval()和function()构造器来避免双重求值带来的性能消耗,

同样的,给setTimeout()和setInterval()传递函数而不是字符串作为参数。

 //双重求值:就是在js代码中执行另一段js代码,不建议使用下面这些方式
 eval('代码') 
 function构造函数--new function('代码')
 setTimeout(‘代码’,100)和setInterval(‘代码’,100) 

2.尽量使用直接量创建对象和数组。直接量的创建和初始化都比非直接量形式要快。

3.避免做重复工作,能节省的步骤就节省。

4.js原生方法总会比你写的任何代码都要快


章节9 构建并部署高性能js应用小结

构建和部署的过程对基于js的web应用的性能有着巨大影响。这个过程中最重要的步骤有:

1.合并、压缩js文件。可使用Gzip压缩,能够减少约70%的体积!

这些都是在构建过程中完成的工作,不要等到运行时去做,webpack也是在构建过程中,完成的工作。

  1. 通过正确设置HTTP响应头来缓存js文件,通过向文件名增加时间戳来避免缓存问题。

  2. 使用CDN提供js文件;CDN不仅可以提升性能,它也为你管理文件的压缩与缓存,。


章节10 工具

当网页变慢时,分析从网络下载的资源以及分析的资源以及分析脚本的运行性能能让你专注于那些最需要优化的地方。

1.使用网络分析工具找出加载脚本和页面中其他资源的瓶颈,这会帮助你决定那些脚本需要延迟加载,或者需要进一步分析。

 检查图片、样式表和脚本的加载过程,以及它们对页面整体加载和渲染的影响。从而针对性的做出优化

2.把脚本尽可能延迟加载,这样做可以加快页面渲染速度,给用户带来更好的体验。

3.确认脚本和其他资源文件的加载过程已经被优化

 这里主要是说文件从服务器的下载速度,比如服务器那边的配置问题之类的。
 栗子:我就被后端坑过。一个js文件就200k ,下载下来需要50秒钟!
 后面发现原来是后端那边nginx没有开启加速配置什么的,导致出现的问题,找问题找半天。 

4.测试脚本的运行时间,用一个Date实例减去另一个Date实例,得到的时间差就是脚本运行消耗的时间。

 let start=new Date();
 //你的代码
 let time=newDate()-start;

5.chrome ,火狐 等主流浏览器的控制面板,已经能够反映很多性能问题。仔细分析就能找出很多问题。例如:资源加载,断点等