• 高性能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不仅可以提高性能,还可以为你管理压缩和缓存
  • 相关阅读:
    three.js raycaster射线碰撞的坑 (当canvas大小 不是屏幕大小是解决拾取物体的办法)
    如何去掉IE文本框后的那个X css代码
    解决input 有readonly属性 各个浏览器的光标兼容性问题
    centos的基本命令03(du 查看文件详情,echo清空文件内容)
    centos的 / ~
    centos的基本命令02
    centos的基本命令01
    关系性数据库和非关系型数据库
    绝对路径和相对路径的理解
    linux的目录和基本的操作命令
  • 原文地址:https://www.cnblogs.com/yiyang/p/4573326.html
Copyright © 2020-2023  润新知