您的位置:首页 > Web前端 > JavaScript

高性能JavaScript

2015-06-13 12:40 393 查看

前言

本文基于《高性能JavaScript》整理而成。

加载和运行

背景

无论是
<script>
标签引用的外部
js
文件,还是内联的
<script>
标签,都会阻塞其他浏览器的处理过程,直到
js
代码被“下载--解析--执行”完成后,才会继续其他进程。

部分高级浏览器已经支持并行下载
js
文件,但浏览器进程仍然需要等待所有
js
文件执行完毕后,才会继续。

动态创建的
<script>
标签不会阻塞页面的解析。

页面解析时,在遇到
<body>
前,页面是空白的。

优化方法

阻塞方式

将所有
<script>
标签放置在页面的底部,仅靠
</body>
的上方。此方法可以保证页面在脚本运行前完成解析。

将脚本成组打包。

页面的
<script>
标签越少,页面的加载速度越快,响应也更加迅速。不论外部脚本文件还是内联代码都是如此。

非阻塞方式

<script>
标签添加
defer
属性(只适用于
IE
Firefox 3.5
以上的版本)

这种方式引入的
js
代码会在
domReady
后执行

动态创建
<scirpt>
元素,用它下载并执行代码

动态创建的
<script>
不会阻塞页面的解析,
js
代码的处理和页面的解析是并行的

ajax
下载代码,注入页面中

ajax
方式的缺点是不能跨域获取
js
代码

数据

详情

作用域链

背景

函数对象

创建函数时,会创建一个函数对象,并创建一个作用域链(内部
[[scope]]属性


每执行一次函数,就创建一个运行上下文

运行上下文也会创建一个作用域链,并将函数对象的作用域链赋值到运行上下文,再新建一个活动对象,置于作用域链的第一个位置。

作用域链:

0:新建的活动对象

1:函数对象的作用域链复制过来

作用域链销毁时,活动对象额一同销毁

作用域链的查找性能

局部变量的访问速度总是最快的,因为它们位于作用域链的第一个位置

而全局变量通常是最慢的(优化的JS引擎在某些情况下可以改变这种状况),因为它们位于作用域链的末端。

优化

在没有优化JS引擎的浏览器中,最好尽可能使用局部变量。用局部变量存储本地范围之外的变量值,如果它们在函数中的使用多余一次

改变作用域链

背景

with


代码流执行到一个
with
表达式时,运行期上下文的作用域链被临时改变。一个新的可变对象被创建,它包含指定对象的所有属性,此对象被推入作用域链的签到,意味着现在函数的所有局部变量被推入第二个作用域链对象中,所有访问代价更高

try catch


catch
块中,会将异常对象推入作用域链签到的一个可变对象中

只要
catch
执行完毕,作用域链会返回到原来的状态

优化

不使用
with


谨慎使用
try catch


可以精简代码最小化
catch
对性能的影响,一个很好的模式是将错误交给一个专用函数来处理。没有局部变量访问,作用域链临时改变不会影响代码的性能。

动态作用域

背景

优化的JS引擎是通过分析静态代码来确定哪些变量应该在任意时刻被访问,企图避开传统的作用域链查找,取代以标识符索引的方式进行快速查找。当涉及一个动态作用域后,此优化方法就不起作用了。引起需要切回慢速的寄语哈希表的标识符识别方法,更像传统的作用域链搜索

优化

避免使用动态作用域

闭包

这里的闭包指的是活动对象里创建的函数对象

外层的执行上下文的作用域链包括:活动对象、全局对象;

闭包的作用域链包括:活动对象、全局对象

外层函数执行完毕后,执行上下文销毁,但活动对象仍然被闭包的作用域链引用,因此不会销毁,这样就有性能开销。尤其在
IE
中更被关注,IE使用非本地
JS
对象实现
DOM
对象,闭包可能导致内存泄露

对象成员

背景

对象成员比直接量或局部变量访问速度慢,在某些浏览器上比访问数组项还慢

对象有两种类型的成员:实例成员和原型成员

hasOwnProperty()
访问的是实例成员

in
访问的是实例+原型成员

增加遍历原型链的开销很大

优化

只在必要情况下使用对象成员

用局部遍历存储对象成员,局部变量要快很多

总结

数据存储位置可以对代码整体性能产生重要影响

四种数据访问类型:

直接量

变量

数组项

对象成员

直接量和局部变量的访问速度非常快,数组项和对象成需要更长时间

避免使用
with
表达式,因为它该变量运行期上下文的作用域链。

小心对的
try-catch
表达式的
catch
语句,因为它有同样的效果

嵌套对象成员会造成重大性能影响,尽量少用

一个属性或方法在原型链中的位置越深,访问它的速度就越慢

一般来说,可以通过以下方法提高性能:

将经常用到的对象成员,数组项和域外变量存入局部变量中,然后,访问局部变量的速度会快于那些原始变量

DOM编程

详情

什么是DOM?

DOM 是与语言无关的API,浏览器中的接口却是以
JavaScript
实现的

浏览器通常要求DOM实现和
JavaScript
实现保持相互独立

IE


JavaScript
实现:位于库
jscript.dll


DOM
实现:位于另一个库
mshtml.dll(内部代号Trident)


Safari


JavaScript
实现:
JavaScriptCore
引擎

DOM
实现:
Webkit
WebCore
处理

Chrome


JavaScript
实现:
V8
引擎

DOM
实现:
Webkit
WebCore
处理

Firefox


JavaScript
实现:
TraceMonkey
引擎

DOM
实现:
Gecko
渲染引擎

DOM天生就慢

两个独立的部分以功能接口连接就会带来性能损耗

DOM访问和修改

访问速度就很慢了,修改更慢

访问的DOM越多,代码的执行速度就越慢

innerHTML
DOM
方法对比

innerHTML
不是标准的,但被支持的很好

DOM
方法有:
document.createElement()


二者的性能差别并不大,但在所有浏览器中,
innerHTML
速度更快,除了最新的基于
WebKit
的浏览器

从性能上没有必要区分二者,更多的是从编码风格、可读性、团队习惯等等方面考虑

节点克隆(
element.cloneNode()


大多数浏览器中,克隆节点更有效率,但提高不多

HTML
集合

指的是如
document.getElementsByTagName()
获得的元素集

具有
length
属性,但不是数组

多次访问元素集的过程中,元素集增删节点,也会即时反映在其
length
属性上

优化方法

用局部变量缓存
length


用局部变量缓存集合中的元素

选取更有效的API

抓取DOM

childNodes


nextSibling


IE
中,
nextSibling
的效率更高,其他情况下,没太多差别

childNodes
firstChild
nextSibling
也会返回注释节点和文本节点,因此每次使用都要判断节点类型,比较麻烦

以下API只返回元素节点(以下API中,IE678只支持
children


children
替代
childNodes
children
更快,因为集合项更少

childElementCount
替代
childNodes.length


firstElementChild
替代
firstChild


lastElementChild
替代
lastChild


nextElementSibling
替代
nextSibling


previousElementSibling
替代
previousSibling


CSS选择器

最新的浏览器有(IE8及以上)

document.querySelectorAll()


返回一个类数组对象,不返回HTML集合,所以返回的节点不呈现文档的“存在性结构”,也就避免了前面的HTML集合所固有的性能问题

重绘和排版

背景

DOM树和渲染数

当浏览器下载完所有的页面HTML标记,javascript、css、图片之后,它解析文件并创建两个内部数据结构:DOM树和渲染树

DOM树表示页面结构,渲染树表示DOM节点如何显示

渲染树中为每个需要显示的DOM树节点至少存放一个节点(隐藏DOM元素在渲染树中没有节点)

重绘和排版是不同的概念

不是所有的DOM改变都会影响几何属性

重绘和排版是负担很重的操作,可能导致网页应用的用户界面失去响应

会引发重排版的操作

小范围影响

添加或删除可见的DOM元素

元素位置改变

元素尺寸改变

内容改变(文本改变或图片被另一个不同尺寸的所替代)

最初的页面渲染

浏览器窗口改变尺寸

影响整个页面的

滚动条出现

查询布局信息

任何查询都会刷新渲染队列,大部分浏览器都会批量处理这些队列

优化

批量修改风格

统一处理

修改CSS的类名

离线操作DOM树

有三个方法可以将DOM从文档中摘除

隐藏元素,然后修改,然后显示

使用文档片断

将原始元素拷贝到一个脱离文档的节点中,修改副本,然后覆盖原始元素

缓存并减少对布局信息的访问

将元素提出动画流

绝对定位

IE和
:hover


不要对大量元素应用
:hover


采用事件托管

总结

最小化DOM访问,在JavaScript端做尽可能多的事情

在反复访问的地方使用局部变量存放DOM引用

小心处理HTML集合

集合总是会对底层文档重新查询

缓存length属性

如果经常操作集合,可以将集合拷贝到数组中

采用更快的API

注意重绘和排版

批量修改风格

离线操作DOM树

缓存并减少对布局信息的访问

动画中使用绝对坐标

使用事件代理最小化句柄数量

算法和流程控制

详情

前言

代码整体结构是执行速度的决定因素之一

代码量少不一定运行速度快,代码量大不一定运行速度慢

四种循环

for


包括四部分:初始化体、前测条件、后执行体、循环体

while


包括两部分:预测试条件、循环体

do while


js
中唯一一种后测试的循环,包括:循环体和后测试条件

for in


用途:枚举任何对象的实例属性和原型属性

循环性能

for in
速度最慢,因为它要查找各种属性

优化

如果要迭代一个有限的、已知的属性列表,使用其他循环类型更快,可使用如下模式(只关注感兴趣的属性):

var props = ["prop1", "prop2"],
i = 0;
while (i < props.length){
process(object[props[i]]);
}


其他循环性能相当

减少迭代的工作量

减少迭代次数

达夫设备

基于函数的迭代

foreach
每次迭代都会调用函数,性能较低

条件表达式

两种条件表达式

if else


switch


如何选择

基于条件数量

易读性:条件数量较大,倾向于使用
switch


性能:
switch
更快

优化
if else


将最常见的条件体放在首位

if else
组织成一系列嵌套的
if else
表达式。使用一个单独的一长串的
if else
通常导致运行缓慢,因为每个条件都要被计算

比如使用二分法

查表法

暂不了解

递归

递归的问题

一个错误定义,或者缺少终结条件可导致长时间运行,冻结用户界面

还会遇到浏览器调用栈大小的限制

优化

任何可以用递归实现的算法都可以用迭代实现。使用优化的循环替代长时间运行的递归函数可以提高性能,因为运行一个循环比反复调用一个函数的开销要低

制表

记录计算过的结果

总结

代码的写法和算法选用会影响
JavaScript
的运行时间。与其他语言不同的是,
JavaScript
可用资源有限,所以优化技术更为重要

for
while
do-while
循环的性能特性相似

除非要迭代一个属性未知的对象,否则不要使用
for-in
循环

改善循环性能的最好办法是减少每次迭代中的运算量,并减少循环迭代次数

一般来说,
switch
总是比
if-else
更快,但并不总是最好的解决办法

当判断条件较多时,查表法比
if-else
或者
switch
更快

浏览器的调用栈尺寸限制了递归算法在
JavaScript
中的应用:栈溢出错误导致其他代码也不能正常执行

如果使用递归,修改为一个迭代算法或者使用制表法可以避免重复工作

运行的代码总量越大,使用这些策略所带来的性能提升就越明显

响应接口

详情

浏览器有一个单独的处理进程,它由两个任务所共享:

JavaScript
任务

用户界面更新任务

每个时刻只有其中的一个操作得以执行,也就是
JavaScript
代码运行时用户界面不能对输入产生反应,反之亦然。管理好
JS
运行时间对网页应用的性能很重要

浏览器 UI 线程

JS
UI
更新共享的进程通常被称作浏览器UI线程。

此UI线程围绕一个简单的队列系统工作,任务被保存到队列中直至进程空闲。一旦空闲,队列中的下一个任务将被检索和运行。这些任务不是运行
JS
代码,就是执行UI更新,包括重绘和排版

浏览器有两个限制

调用栈尺寸限制

长时间脚本限制

每个浏览器对长运行脚本检查方法上略有不同

多久算“太久”?

一个单一的
JS
操作应当使用的总时间(最大)是100毫秒

用定时器让出时间片

如果有些
JS
任务因为复杂性原因不能在100毫秒或更少的时间内完成,这种情况下,理想方法是让出对UI线程的控制,让UI更新可以进行,让出控制意味着停止
JS
运行,给UI线程机会进行更新,然后再运行`JS

定时器
setTimeout
到达时间后,只是加入队列,并不是执行

定时器精度

浏览器的定时器不是精确的,通常会发生几毫秒偏移

windows 系统上定时器分辨率为15毫秒

定时器小于15将在IE中导致浏览器锁定,所以最小值建议为25毫秒(实际时间是15或30)以确保至少15毫秒延迟

大多数浏览器在定时器延时小于10毫秒时表现出差异性

在数组处理中使用定时器

循环优化技巧如果还不能达到目标,可以考虑使用定时器,考虑以下条件:

处理过程必须是同步处理吗?

数据必须按顺利处理吗?

如果上述答案都是否,则可以使用定时器优化

分解任务

如果一个函数运行时间太长,可以考虑分解趁改一系列能够短时间完成的较小的函数,把独立方法放在定时器中调用。将每个函数放入一个数组,然后用上面讲到的数组处理模式。

限时运行代码

根据以上描述,每次定时器只执行一个任务效率不高。

优化方法是:每次定时器执行多个任务,设定时间限制小于50毫秒即可(
do-while
循环)

定时器性能

低频率的重复定时器(间隔在1秒或1秒以上),几乎不影响整个网页应用的响应

多个重复定时器使用更高的频率(间隔在100到200毫秒之间)性能更低

优化

限制高频率重复定时器的数量

创建一个单独的重复定时器,每次执行多个操作

网络工人线程

暂无

总结

JavaScript
和用户界面更新在同一个进程内运行,同一时刻只有其中一个可以运行。有效地管理UI线程就是要确保
JavaScript
不能运行太长时间,一面影响用户体验。因此要注意:

JavaScript
运行时间不应该超过100毫秒,过长的运行时间导致UI更新出现可察觉的延迟,从而对整体用户体验产生负面影响

JavaScript
运行期间,浏览器响应用户交互的行为存在差异,无论如何,
JavaScript
长时间运行将导致用户体验混乱和脱节

定时器可以用于安排代码推迟执行,它使得你可以将长运行脚本分解成一系列较小的任务

网络工人线程是新式浏览器才支持的特性,它允许你在UI线程之外运行
JavaScript
代码而避免锁定UI

网络应用程序越复杂,积极主动地管理UI线程就越显得重要。没有什么
JavaScript
代码可以重要到允许影响用户体验的程度

异步JavaScript

详情

有五种常用技术用于向服务器请求数据

XMLHttpRequest(XHR)
(常用)

动态脚本标签插入
(常用)

Multipart XHR
(常用)

iframes
(不常用)

Comet
(不常用)

XHR


就是
ajax


不能跨域

可以选择
GET
POST


GET


如果不改变服务器状态只是取回数据,则使用
GET


GET
请求会被缓存

POST


当URL和参数的长度超过了2048个字符时才使用
POST
提取数据

动态脚本插入(
jsonp


可以跨域

只能通过
GET
方法传递,不能用
POST


对服务器返回的数据格式有要求

Multipart XHR


暂略

如果只向服务器发送数据,有两种技术

XHR


XHR
主要用于从服务器获取数据,它也可以用来向服务器发送数据

可以用
GET
POST
方式发送数据,以及任意数量的HTTP信息头。这样灵活性大。当数据量超过浏览器的最大URL长度时,
XHR
特别有用。这时候可以用
POST
方式发送数据

向服务器发送数据时,
GET
POST
快。

GET
请求要占用一个单独的数据包

POST
至少要发送两个数据包,一个用于信息头,一个是POST体

灯标

和动态脚本标签插入类似,用新的
Image
对象,将src设置为服务器上一个脚本文件的URL

Image
对象不必插入DOM节点

这是将信息发回服务器的最有效方法。开销最小,而且任何服务器端错误都不会影响客户端

限制

不能发送
POST
数据

除了
onload
,很少能获取服务器返回的信息

数据格式

越轻量级的格式越好,最好是
JSON
和字符分隔的自定义格式。数据量大的话,就用这两种格式

其他优化技术

避免发出不必要的
Ajax
请求

在服务端,设置
HTTP
头,确保返回报文被缓存在浏览器中

在客户端,于本地缓存已获取的数据,不要多次请求同一个数据

服务端

如果想要缓存
Ajax
响应报文,客户端发起请求必须使用
GET
方法

设置
Expires


总结

高性能
Ajax
包括:知道你项目的具体需求,选择正确的数据格式和与之相配的传输技术

数据格式

纯文本和HTML是高度限制的,但它们可节省客户端的CPU周期

XML被广泛支持,但它非常冗长且解析缓慢

JSON是轻量级的,解析迅速(作为本地代码而不是字符串),交互性与XML相当

字符分隔的自定义格式非常轻量,在大量数据解析时速度最快,但要额外地编写程序在服务端构造格式,并在客户端解析

请求数据

XHR
提供最完善的控制和灵活性,尽管它将所有传入数据视为一个字符串,这有可能降低解析速度

jsonp
允许跨域,但接口不够安全,而且不能读取信息头或响应报文代码

MXHR
可以减少请求的数量,一次响应中处理不同的文件类型,尽管它不能缓存收到的响应报文

发送数据

图像灯标是最简单和最有效的方法

XHR
也可以用
POST
方法发送大量数据

其他准则提高
Ajax
的速度

减少请求数量,可通过
JavaScript
CSS
打包,或者使用
MXHR


缩短页面的加载时间,在页面其他内容加载之后,使用
Ajax
获取少量重要文件

确保代码错误不要直接显示给用户,并在服务器端处理错误

学会何时使用一个健壮的
Ajax
库,何时编写自己的底层
Ajax
代码

Ajax
是提升网站性能的最大的改进区域之一

编程实践

详情

避免二次评估

JavaScript
允许在程序中获取一个包含代码的字符串然后运行它

有四种标准方法可以实现

eval_r()


Function()
构造器

setTimeout()


setInterval()


这样的话,会有两步:字符串首先被评估为正常代码,然后执行过程中,运行字符串中的代码时发生另一次评估。二次评估是昂贵的操作

使用对象/数组直接量

不要重复工作

不要做不必要的工作

不要重复已经完成的工作

延迟加载

使用速度快的部分

引擎通常是处理过程中最快的部分,实际上速度慢的是你的代码

位操作运算符

暂略

使用原生方法

内置的
Math
属性

Math.E


Math.LN10


Math.LN2


Math.LOG2E


Math.LOG10E


Math.PI


Math.SQRT1_2


Math.SQRT2


内置的
Math
方法

Math.abs(num)


Math.exp(num)


Math.log(num)


Math.pow(num, power)


Math.sqrt(num)


Math.acos(x)


Math.asin(x)


Math.atan(x)


Math.atan2(y, x)


Math.cos(x)


Math.sin(x)


Math.tan(x)


原生的CSS选择器API

querySelector()


querySelectorAll()


总结

避免使用
eval_r()
Function()
构造器避免二次评估,此外,给
setTimeout()
setInterval()
传递函数参数而不是字符串参数

创建新对象和数组时使用对象直接量和数组直接量。它们比非直接量形式创建和初始化更快

避免重复进行相同工作。当需要检测浏览器时,使用延迟加载或条件预加载

执行数学运算时,考虑使用位操作,它直接在数字底层进行操作

原生方法总是比
JavaScript
写的东西要快。尽量使用原生方法。

创建并部署高性能JavaScript应用程序

合并
JavaScript
文件,减少
HTTP
请求的数量

压缩
JS
文件

通过设置HTTP

相应报文头使
JS
文件可缓存,通过向文件名附加时间戳解决缓存问题

使用CDN提供
JS
文件,CDN不仅可以提高性能,还可以为你管理压缩和缓存
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: