JavaScript高性能

数据存取

作用域链

  • 每一个函数都是Function对象的一个实例,每个实例上都包含一个内部属性[[Scope]]包含一个被创建函数的作用域的集合
  • 当函数被创建时,它的作用域链插入一个变量,这个变量包含着所有在全局范围内定义的变量(假设该函数定义在全局环境上)
  • 当执行函数时,会创建一个执行环境(上下文),每次函数的执行环境是独一无二的,即单独开辟一块内存,函数执行完毕,执行环境就会被销毁
  • 每个执行环境都有自己的作用域链,用于解析标识符。当执行环境被创建时,会创建当前执行环境的活动对象,这个活动对象包含当前函数的局部变量(按照出现的顺序),命名参数,参数集合(arguments)以及this。活动对象被推入作用域链的最前端,即当前作用域链包含一个活动对象以及全局对象。
  • 在函数执行过程中,每遇到一个变量,就回去函数的作用域链中去寻找,先在活动对象中寻找,没有则去全局对象中寻找。每个标识符都要经历这样的搜索过程,正式这一过程影响了性能

闭包的作用域链

  • 与上述作用域链相同,只是在闭包被创建时,闭包的[[Scope]]属性包含了与当前执行环境作用域链相同的对象引用,因此当外部函数执行完毕后,作用域链由于仍被闭包引用,所以不会被销毁,也就产生了闭包这一特性,也就存在更多的内存开销
  • 当闭包代码执行,会创建一个执行环境,同时创建一个自身的活动对象,连同上述的[[Scope]]所引用的作用域链一同初始化,因此,闭包中使用的外部标识符,实际上是跨作用域链查询获得的,在频繁访问时,会造成性能损失

try catch

try代码块发生错误,执行过程自动跳到catch子句,然后把异常对象推入一个变量对象并至于作用域链的首位,函数代码块中的局部变量将会放在第二个作用域对象中

对象成员

  • JavaScript中的对象是基于原型的,对象内有一个内部属性绑定到它的原型。因此,对象有两种成员类型:实例成员(也成为own成员)和原型成员
  • 对象的原型决定了实例的类型。默认情况下,所有对象都是对象(Object)的实例。

访问对象成员的速度比访问字面量或变量要慢

对象在原型链中存在的位置越深,找到它也就越慢

只在必要时使用对象成员

DOM编程

innerHTML在各大浏览器中要普遍快于document.createElement方法,而在最新版的WebKit浏览器中innerHTML则相对较慢

在大多数浏览器中,节点克隆element.cloneNode()都要更有效率,但也不是特别明显

HTML集合,包含了DOM节点引用的类数组对象。HTML集合一直与文档保存着链接,每次访问时,都会去重复执行查询过程。因此,读取一个集合的length比读取一个普通数组要慢的多,因为每次都要去重新查询。

document.getElementByName()
document.getElementByTagName()
document.getElementByTagName()

document.querySelectorAll()使用CSS选择器作为参数并返回一个NodeList——包含匹配节点的类数组对象。该方法不会返回HTML集合,因此也避免了之前的性能问题。document.querySelector()获取第一个匹配节点。

DOM元素的属性诸如childNodes,firstChild,nextSibling,并不区别元素节点和其他类型节点,但有时我们只需要元素节点。可以使用children,childrenElementCount,firstElementChild,lastElementChild,previousElementSibling,nextElementSibling等方法。

重绘和重排

重排何时发生

  • 添加或删除DOM元素
  • 元素的位置改变
  • 元素的尺寸改变
  • 内容改变
  • 页面渲染初始化
  • 浏览器窗口尺寸改变

由于每次重排会产生计算消耗,大多数浏览器通过队列化修改并批量执行来优化重排过程。然而调用与上面发生重排条件相关的方法,会强制出发浏览器重排,因为浏览器需要返回最新的值

使DOM脱离文档流的三种方法:

  • 隐藏元素,修改应用,重新显示。设置diplay:none;属性,使元素脱离DOM树,避免重排。
  • 使用文档片段(document fragment)在当前文档外构建一棵子树,再把它拷贝回文档。文档片段一个便利的语法特性是当你附加一个片段到节点中时,实际上被附加的是该片段的子节点。
  • 将原始元素拷贝到一个脱离文档的节点中,修改后进行替换。利用cloneNode()方法。

元素脱离动画流,避免其他页面受到影响进而重排:

  • 使用绝对位置定位页面上的动画元素,将其脱离文档流
  • 让元素动起来。当他扩大时,会覆盖一部分页面。只会导致部分页面被重绘,而不会重排。
  • 当动画结束时恢复定位,从而只会下移一次其他文档元素

:hover在子元素很多的情况下,应该避免使用

算法和流程控制

循环

  • 通过提取数组的长度并保存在一个变量中来提升性能
  • 倒序遍历数组
  • 避免使用for-in循环,除非你要遍历一个属性数量未知的对象

当循环的复杂度为O(n)时,减少每次迭代的工作量是最有效的方法。当复杂度大于O(n)时,建议着重减少迭代次数。达夫算法

条件语句

使用if-else还是switch,条件数量越大,越倾向于switch而不是if-else。在可读性上,也是同样的。

switch语句比较值时使用全等操作符,不会发生类型转换的损耗

switch更适合每个键都需要对应一个独特的动作或一系列动作的场合

优化方法:

  • if-else,使用if-else进行嵌套,即二分法
  • 采用数组作为查找表来替代

递归

运行一个循环比反复调用一个函数的开销要小的多,结合函数执行的作用域链理解

递归算法改为迭代算法,可以避免堆栈溢出错误

使用Memorization,避免重复计算

字符串和正则表达式

字符串合并

+和+=性能高于String.prototype.concat

正则表达式优化

匹配过程

  1. 编译。浏览器验证表达式,并把它编译成一个原生代码程序,执行匹配工作。把正则对象赋值给一个变量,可以避免重复编译操作。
  2. 设置起始位置。确定目标字符串的起始搜索位置,这个位置由lastIndex指定或是字符串的起始位置。从第四步返回这里时,此位置在最后一次匹配的起始位置的下一个位置上。
  3. 匹配每个正则表达式字元。确认起始位置后,会逐个检查文本和正则表达式模式。当一个特定的字元匹配失败时,正则表达式会试着回溯到之前尝试匹配的位置上,尝试其他可能路径。
  4. 匹配成功或失败。若匹配失败,正则表达式会退回第二步,然后从下一个字符重新尝试。

回溯,正则表达式在遇到量词或分支时,会生成一个决策点,然后继续匹配之后的字符,若匹配失败,则返回这个决策点,在剩余的选项中选择一个重新匹配,直到正则表达式中量词和分支选项的所有排列组合都失败,它将放弃匹配,转而移动到字符串的下一个字符,重复此过程。

提高正则表达式效率的方法

  • 关注如何让匹配失败的更快。正则表达式匹配失败的位置比成功的位置要多得多
  • 正则表达式以简单,必须的字元开始。如果可能的话,避免以分组或选择字元开头
  • 使用量词模式,使它们后面的字元互斥。尽量具体化你的匹配模式
  • 减少分支数量,缩小分支范围
  • 使用非捕获组
  • 暴露必须的字元
  • 使用合适的量词。贪婪和惰性量词的使用
  • 把正则表达式赋给变量以避免对它们重新编译
  • 将复杂的正则表达式拆分为简单的片段。避免在一个正则表达式中处理太多的任务。
  • 活用charAt,slice,substr,substring,indexOf,lastIndexOf方法

快速响应的用户界面

浏览器对JavaScript运行时间的限制

  • 调用栈大小限制
  • 长时间运行脚本限制。分为两种,一种记录脚本开始以来执行语句的数量。另一种记录语句执行的总时间。

各种浏览器的行为大致相同。当脚本执行时,UI不随用户交互而更新,而用户交互行为所引发的JavaScript任务被加入队列中,而这段时间内由用户交互行为引发的UI更新会被自动跳过,因为页面中动态的变化会被优先考虑。

限制所有的javaScript任务在100毫秒或更短的时间内完成

使用定时器让出时间片段

定时器与UI线程的交互方式有助于把运行耗时较长的脚本拆分为较短的片段

定时器的setTimeout()setInterval()的第二个参数,是经过指定时常后把函数添加到任务队列,而不是在这段时间后执行。这个任务会等待队列中的其他任务执行完后执行

创建一个定时器会造成UI线程暂停,如同它从一个任务切换到下一个任务。

普遍来讲,最好使用至少25毫秒,因为再小的延时,对大多数UI更新来说不够用

分割任务

如果一个函数运行时间太长,那么检查一下是否可以把它拆分为一系列能在较短时间内完成的子函数

// steps 要处理函数的集合, args 函数参数的集合, callback 处理完成的回调函数
function multistep(steps, args, callback) {
	const tasks = steps.conact();
    setTimeout(() => {
        // 执行下一个任务,判断时间是否小于50ms
        const start = +new Date();
        do {
            const task = tasks.shift();
        	task.apply(null, args||[]);
        } while (todo.length > 0 && (+new Date() - start < 50));
        
        // 看是否还有其他任务
        if (tasks.length > 0) {
            setTimeout(arguments.callee, 25);
        } else {
            callback();
        }
    }, 25);
}

使用此函数的前提条件是:任何可以异步处理而不影响用户体验或造成相关代码错误

在web应用中限制高频率重复定时器的数量。建议创建一个独立的重复定时器,每次执行多个操作

Web Workers适用于那些处理纯数据,或者与浏览器UI无关的长时间运行脚本

  • 编码/解码大字符串
  • 复杂数学运算(包括图像或视频处理)
  • 大数组排序

Ajax

数据传输

XMLhttpRequest是目前最常用的技术,它允许异步发送和接收数据。但不能跨域

动态脚本注入,可以跨域请求数据。是一个Hack。

Multipart XHR

允许客户端只用一个HTTP请求就可以从服务端向客户端传送多个资源。缺点是资源不能被缓存,因为合并后的资源是作为字符串传输的,然后被JavaScript代码分解成片段

发送数据

  • 利用XHR。灵活,能够对请求方法进行设置,并对结果进行处理。
  • 信标Beacons。类似于动态脚本注入。创建一个Image对象,并把src属性设置为服务器上脚本的URL。但无法发送POST数据,且URL长度有最大值,能接收到的响应类型是有限的。如果不需要在响应中返回数据,就应该发送一个不带消息正文的204 No Content 状态码。

数据格式

  • JSON

    标准JSON

    简化JSON,属性名简化为一个字母

    数组JSON,属性名完全省略,大小最小,下载最快,解析速度最快

  • JSON-P(JSON with padding)

    文件大小和下载耗时与JSON格式基本相同,而解析速度几乎快了10倍。但要注意,不要把任何敏感数据编码在JSON-P中,因为JSON-P必须是可执行的JavaScript

  • 自定义数据格式。使用split()解析。对于非常大的数据集,它是最快的格式

Ajax性能指南

缓存信息

在服务端,设置HTTP头信息以确保你的相迎会被浏览器缓存。必须以GET方式发出请求,设置Expires头信息

在客户端,把获取的信息存储在本地

编程实践

使用Object/Array直接量,直接量运行较快,且节省代码。对象属性和数组项越多,使用直接量的好处越明显。

避免重复工作。延迟加载和条件预加载,例如检测浏览器的方法

使用速度快的部分。位操作,在进行数学计算时,考虑直接操作数字的二进制形式运算

原生方法。特别是数学运算和DOM操作

构建并部署高性能JavaScript应用

  • 减少HTTP请求数,合并相关的JavaScript资源文件
  • 预处理JavaScript文件。预处理你的JavaScript源文件并不会让应用变得更快,但它允许你做些其他的事情,例如有条件的插入测试代码,来衡量应用程序的性能
  • JavaScript压缩(Gzip编码)。把JS文件中所有与运行无关的部分进行剥离的过程。服务器端压缩
  • JavaScript的HTTP压缩。
  • 缓存JavaScript文件
  • 使用内容分发网络CDN
posted @ 2019-06-06 17:43  CodingSherlock  阅读(486)  评论(0编辑  收藏  举报