前言
本文基于《高性能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 是与语言无关的API,浏览器中的接口却是以
- 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
- 重绘和排版
- 背景
- DOM树和渲染数
- 当浏览器下载完所有的页面HTML标记,javascript、css、图片之后,它解析文件并创建两个内部数据结构:DOM树和渲染树
- DOM树表示页面结构,渲染树表示DOM节点如何显示
- 渲染树中为每个需要显示的DOM树节点至少存放一个节点(隐藏DOM元素在渲染树中没有节点)
- 重绘和排版是不同的概念
- 不是所有的DOM改变都会影响几何属性
- 重绘和排版是负担很重的操作,可能导致网页应用的用户界面失去响应
- 会引发重排版的操作
- 小范围影响
- 添加或删除可见的DOM元素
- 元素位置改变
- 元素尺寸改变
- 内容改变(文本改变或图片被另一个不同尺寸的所替代)
- 最初的页面渲染
- 浏览器窗口改变尺寸
- 影响整个页面的
- 滚动条出现
- 小范围影响
- 查询布局信息
- 任何查询都会刷新渲染队列,大部分浏览器都会批量处理这些队列
- DOM树和渲染数
- 优化
- 批量修改风格
- 统一处理
- 修改CSS的类名
- 离线操作DOM树
- 有三个方法可以将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
提取数据
- 当URL和参数的长度超过了2048个字符时才使用
- 就是
- 动态脚本插入(
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不仅可以提高性能,还可以为你管理压缩和缓存