高性能JavaScript

前言

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

加载和运行

背景

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

优化方法

  • 阻塞方式
    • 将全部<script>标签放置在页面的底部,仅靠</body>的上方。此方法能够保证页面在脚本运行前完成解析。
    • 将脚本成组打包。css

      页面的<script>标签越少,页面的加载速度越快,响应也更加迅速。不论外部脚本文件仍是内联代码都是如此。
  • 非阻塞方式
    • <script>标签添加defer属性(只适用于IEFirefox 3.5以上的版本)html

      这种方式引入的js代码会在domReady后执行
    • 动态建立<scirpt>元素,用它下载并执行代码java

      动态建立的<script>不会阻塞页面的解析,js代码的处理和页面的解析是并行的
    • ajax下载代码,注入页面中ajax

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

数据

详情

  • 做用域链
    • 背景
      • 函数对象编程

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

      • 每执行一次函数,就建立一个运行上下文windows

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

        做用域链:
        • 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不只能够提升性能,还能够为你管理压缩和缓存
相关文章
相关标签/搜索