高性能JavaScript

前言

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

加载和运行

背景

  • 无论是<script>标签引用的外部js文件,还是内联的<script>标签,都会阻塞其他浏览器的处理过程,直到js代码被“下载--解析--执行”完成后,才会继续其他进程。
  • 部分高级浏览器已经支持并行下载js文件,但浏览器进程仍然需要等待所有js文件执行完毕后,才会继续。
  • 动态创建的<script>标签不会阻塞页面的解析。
  • 页面解析时,在遇到<body>前,页面是空白的。

优化方法

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

    • 将脚本成组打包。

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

  • 非阻塞方式
    • <script>标签添加defer属性(只适用于IEFirefox 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实现:WebkitWebCore处理
      • Chrome
        • JavaScript实现:V8引擎
        • DOM实现:WebkitWebCore处理
      • Firefox
        • JavaScript实现:TraceMonkey引擎
        • DOM实现:Gecko渲染引擎
    • DOM天生就慢
      • 两个独立的部分以功能接口连接就会带来性能损耗
  • DOM访问和修改
    • 访问速度就很慢了,修改更慢
    • 访问的DOM越多,代码的执行速度就越慢
  • innerHTMLDOM方法对比
    • innerHTML不是标准的,但被支持的很好
    • DOM方法有:document.createElement()
    • 二者的性能差别并不大,但在所有浏览器中,innerHTML速度更快,除了最新的基于WebKit的浏览器
    • 从性能上没有必要区分二者,更多的是从编码风格、可读性、团队习惯等等方面考虑
  • 节点克隆(element.cloneNode()
    • 大多数浏览器中,克隆节点更有效率,但提高不多
  • HTML集合
    • 指的是如document.getElementsByTagName()获得的元素集
    • 具有length属性,但不是数组
    • 多次访问元素集的过程中,元素集增删节点,也会即时反映在其length属性上
    • 优化方法
      • 用局部变量缓存length
      • 用局部变量缓存集合中的元素
  • 选取更有效的API
    • 抓取DOM
      • childNodes
      • nextSibling
      • IE中,nextSibling的效率更高,其他情况下,没太多差别
      • childNodesfirstChildnextSibling也会返回注释节点和文本节点,因此每次使用都要判断节点类型,比较麻烦
      • 以下API只返回元素节点(以下API中,IE678只支持children
        • children替代childNodeschildren更快,因为集合项更少
        • 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可用资源有限,所以优化技术更为重要
  • forwhiledo-while 循环的性能特性相似
  • 除非要迭代一个属性未知的对象,否则不要使用for-in循环
  • 改善循环性能的最好办法是减少每次迭代中的运算量,并减少循环迭代次数
  • 一般来说,switch总是比if-else更快,但并不总是最好的解决办法
  • 当判断条件较多时,查表法比if-else或者switch更快
  • 浏览器的调用栈尺寸限制了递归算法在JavaScript中的应用:栈溢出错误导致其他代码也不能正常执行
  • 如果使用递归,修改为一个迭代算法或者使用制表法可以避免重复工作
  • 运行的代码总量越大,使用这些策略所带来的性能提升就越明显

响应接口

详情

  • 浏览器有一个单独的处理进程,它由两个任务所共享:
    • JavaScript 任务
    • 用户界面更新任务
    • 每个时刻只有其中的一个操作得以执行,也就是JavaScript代码运行时用户界面不能对输入产生反应,反之亦然。管理好JS运行时间对网页应用的性能很重要
  • 浏览器 UI 线程
    • JSUI更新共享的进程通常被称作浏览器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
    • 不能跨域
    • 可以选择GETPOST
    • GET
      • 如果不改变服务器状态只是取回数据,则使用GET
      • GET请求会被缓存
    • POST
      • 当URL和参数的长度超过了2048个字符时才使用POST提取数据
  • 动态脚本插入(jsonp
    • 可以跨域
    • 只能通过GET方法传递,不能用POST
    • 对服务器返回的数据格式有要求
  • Multipart XHR
    • 暂略
  • 如果只向服务器发送数据,有两种技术
    • XHR
      • XHR主要用于从服务器获取数据,它也可以用来向服务器发送数据
      • 可以用GETPOST方式发送数据,以及任意数量的HTTP信息头。这样灵活性大。当数据量超过浏览器的最大URL长度时,XHR特别有用。这时候可以用POST方式发送数据
      • 向服务器发送数据时,GETPOST快。
        • 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的速度
    • 减少请求数量,可通过JavaScriptCSS打包,或者使用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不仅可以提高性能,还可以为你管理压缩和缓存
posted @ 2015-06-13 12:40  一扬  阅读(182)  评论(0编辑  收藏  举报