美文网首页菜鸟学编程
前端性能优化之 JavaScript

前端性能优化之 JavaScript

作者: 菜鸟飞不动 | 来源:发表于2019-06-18 00:00 被阅读1次

    前言

    本文为 《高性能 JavaScript》 读书笔记,是利用中午休息时间、下班时间以及周末整理出来的,此书虽有点老旧,但谈论的性能优化话题是每位同学必须理解和掌握的,业务响应速度直接影响用户体验。

    一、加载和运行

    大多数浏览器使用单进程处理 UI 更新和 JavaScript 运行等多个任务,而同一时间只能有一个任务被执行

    脚本位置

    将所有script标签放在页面底部,紧靠</body>上方,以保证页面脚本运行之前完成解析

    <html>
      <head> </head>
      <body>
        <p>Hello World</p>
        <!--  -->
        <script type="text/javascript" src="file.js"></script>
      </body>
    </html>
    

    defer & async

    常规script脚本浏览器会立即加载并执行,异步加载使用asyncdefer
    二者区别在于aysnc为无序,defer会异步根据脚本位置先后依次加载执行

    <!-- file1、file2依次加载 -->
    <script type="text/javascript" src="file1.js" defer></script>
    <script type="text/javascript" src="file2.js" defer></script>
    <!-- file1、file2无序加载 -->
    <script type="text/javascript" src="file1.js" async></script>
    <script type="text/javascript" src="file2.js" async></script>
    

    动态脚本

    无论在何处启动下载,文件的下载和运行都不会阻塞其他页面处理过程。你甚至可以将这些代码放在<head>部分而不会对其余部分的页面代码造成影响(除了用于下载文件的 HTTP 连接)

    var script = document.createElement("script");
    script.type = "text/javascript";
    script.src = "file1.js";
    document.getElementsByTagName("head")[0].appendChild(script);
    

    监听加载函数

    function loadScript(url, callback) {
      var script = document.createElement("script");
      script.type = "text/javascript";
      if (script.readyState) {
        //IE
        script.onreadystatechange = function() {
          if (script.readyState == "loaded" || script.readyState == "complete") {
            script.onreadystatechange = null;
            callback();
          }
        };
      } else {
        //Others
        script.onload = function() {
          callback();
        };
      }
      script.src = url;
      document.getElementsByTagName("head")[0].appendChild(script);
    }
    

    XHR 注入

    前提条件为同域,此处与异步加载一样,只不过使用的是 XMLHttpRequest


    总结

    • 将所有script标签放在页面底部,紧靠 body 关闭标签上方,以保证页面脚本运行之前完成解析
    • 将脚本成组打包,页面 script 标签越少加载越快,响应也就更迅速。不论外部脚本文件或者内联代码都是如此

    二、数据访问

    数据存储在哪里,关系到代码运行期间数据被检索到的速度.每一种数据存储位置都具有特定的读写操作负担。大多数情况下,对一个直接量和一个局部变量数据访问的性能差异是微不足道的。

    在 JavaScript 中有四种基本的数据访问位置:

    • 直接量
      直接量仅仅代表自己,而不存储于特定位置。 JavaScript 的直接量包括:字符串,数字,布尔值,对象,数组,函数,正则表达式,具有特殊意义的空值,以及未定义
    • 变量
      使用 var / let 关键字创建用于存储数据值
    • 数组项
      具有数字索引,存储一个 JavaScript 数组对象
    • 对象成员
      具有字符串索引,存储一个 JavaScript 对象

    总结

    • 直接量与局部变量访问速度非常快,数组项和对象成员需要更长时间
    • 局部变量比域外变量访问速度快,因为它位于作用域链的第一个对象中。变量在作用域链的位置越深,访问所需要的时间越长。全局变量总是最慢的,因为它们总位于作用域链的最后一环。
    • 避免使用 with 表达式,因为它改变了运行期上下文的作用域链,谨慎对待 try-catch 表达式中 catch 子句,因为它具有同样的效果
    • 嵌套对象成员会造成重大性能影响,尽量少用
    • 属性在原型链中的位置越深,访问速度越慢
    • 将对象成员、数组项、域外变量存入局部变量能提高 js 代码的性能

    三、dom 编程

    对 DOM 操作代价昂贵,在富网页应用中通常是一个性能瓶颈。通常处理以下三点

    • 访问和修改 DOM 元素

    • 修改 DOM 元素的样式,造成重绘和重新排版

    • 通过 DOM 事件处理用户响应

      一个很形象的比喻是把 DOM 看成一个岛屿,把 JavaScript(ECMAScript)看成另一个岛屿,两者之间以一座收费桥连接(参见 John Hrvatin,微软,MIX09,http://videos.visitmix.com/MIX09/T53F)。每次 ECMAScript 需要访问 DOM 时,你需要过桥,交一次“过桥费”。你操作 DOM 次数越多,费用就越高。一般的建议是尽量减少过桥次数,努力停留在 ECMAScript 岛上。

    DOM 访问和修改

    访问或修改元素最坏的情况是使用循环执行此操作,特别是在 HTML 集合中使用循环

    function innerHTMLLoop() {
      for (var count = 0; count < 15000; count++) {
        document.getElementById("here").innerHTML += "a";
      }
    }
    

    此函数在循环中更新页面内容。这段代码的问题是,在每次循环单元中都对 DOM 元素访问两次:一次
    读取 innerHTML 属性能容,另一次写入它


    优化如下

    function innerHTMLLoop2() {
      var content = "";
      for (var count = 0; count < 15000; count++) {
        content += "a";
      }
      document.getElementById("here").innerHTML += content;
    }
    

    你访问 DOM 越多,代码的执行速度就越慢。因此,一般经验法则是:轻轻地触摸 DOM,并尽量保持在 ECMAScript 范围内

    节点克隆

    使用 DOM 方法更新页面内容的另一个途径是克隆已有 DOM 元素,而不是创建新的——即使用 element.cloneNode()(element 是一个已存在的节点)代替 document.createElement();

    当布局和几何改变时发生重排版,下述情况会发生:

    • 添加或删除可见的 DOM 元素
    • 元素位置改变
    • 元素尺寸改变(边距、填充、边框宽度、宽、高等属性)
    • 内容改变(文本或者图片被另一个不同尺寸的所替代)
    • 最初的页面渲染
    • 浏览器窗口尺寸改变

    减少重排次数

    • 改变 display 属性,临时从文档上移除然后再恢复
    • 在文档之外创建并更新一个文档片段,然后将它进行附加
    • 先创建更新节点的副本,再操作副本,最后用副本更新老节点

    总结

    • 最小化 DOM 访问,在 JavaScript 端做尽可能多的事情
    • 在反复访问的地方使用局部变量存放 dom 引用
    • 谨慎处理 HTML 集合,因为它们表现‘存在性’,总对底层文档重新查询。将 length 属性缓存到一个变量中,在迭代中使用这个变量。如果经常操作这个集合,可以将集合拷贝到数组中
    • 如果可以,使用速度更快的 API,比如 document.querySelectorAll()和 firstElementChild()
    • 注意重绘和重排,批量修改风格,离线操作 DOM,缓存或减少对布局信息的访问
    • 动画中使用绝对坐标,使用拖放代理
    • 使用事件托管技术中的最小化事件句柄数量

    四、算法与流程控制

    代码整体结构是执行速度的决定因素之一。代码量少不一定执行快,代码量多,也不一定执行慢,性能损失与代码组织方式和具体问题解决办法直接相关。

    Loops

    在大多数编程语言中,代码执行时间多数在循环中度过。在一系列编程模式中,循环是最常见的模式之一,提高性能必须控制好循环,死循环和长时间循环会严重影响用户体验。

    Types of Loops

    • for
    • while
    • do while
    • for in

    前三种循环几乎所有编程语言都能通用,for in 循环遍历对象命名属性(包括自有属性和原型属性)

    Loop Performance

    循环性能争论的源头是应当选用哪种循环,在 JS 中 for-in 比其他循环明显要慢(每次迭代都要搜索实例或原型属性),除非对数目不详的对象属性进行操作,否则避免使用 for-in。除开 for-in,选择循环应当基于需求而不是性能

    减少每次迭代的操作总数可以大幅提高循环的整体性能

    优化循环:

    • 减少对象成员和数组项的查找,比如缓存数组长度,避免每次查找数组 length 属性
    • 倒序循环是编程语言中常用的性能优化方法

    编程中经常会听到此说法,现在来验证一下,测试样例

    var arr = [];
    for (var i = 0; i < 100000000; i++) {
      arr[i] = i;
    }
    var start = +new Date();
    for (var j = arr.length; j > -1; j--) {
      arr[j] = j;
    }
    console.log("倒序循环耗时:%s ms", Date.now() - start); //约180 ms
    var start = +new Date();
    for (var j = 0; j < arr.length; j++) {
      arr[j] = j;
    }
    console.log("正序序循环耗时:%s ms", Date.now() - start); //约788 ms
    
    循环正反序测试

    基于函数的迭代

    尽管基于函数的迭代显得更加便利,它还是比基于循环的迭代要慢一些。每个数组项要关联额外的函数调用是造成速度慢的原因。在所有情况下,基于函数的迭代占用时间是基于循环的迭代的八倍,因此在关注执行时间的情况下它并不是一个合适的办法。

    条件表达式

    if-else VS switch

    使用 if-else 或者 switch 的流行理论是基于测试条件的数量:条件数量较大,倾向使用 switch,更易于阅读
    当条件体增加时,if-else 性能负担增加的程度比 switch 更多。
    一般来说,if-else 适用于判断两个离散的值或者几个不同的值域,如果判断条件较多 switch 表达式将是更理想的选择

    优化 if-else

    • 最小化找到正确分支:将最常见的条件放在首位
    • 查表法 当使用查表法时,必须完全消除所有条件判断,操作转换成一个数组项查询或者一个对象成员查询。

    递归

    会受浏览器调用栈大小的限制

    迭代

    任何可以用递归实现的算法可以用迭代实现。使用优化的循环替代长时间运行的递归函数可以提高性能,因为运行一个循环比反复调用一个函数的开销要低

    斐波那契

    function fibonacci(n) {
      if (n === 1) return 1;
      if (n === 2) return 2;
      return fibonacci(n - 1) + fibonacci(n - 2);
    }
    

    制表

    //制表
    function memorize(fundamental, cache) {
      cache = cache || {};
      var shell = function(args) {
        if (!cache.hasOwnProperty(args)) {
          cache[args] = fundamental(args);
        }
        return cache[args];
      };
      return shell;
    }
    //动态规划
    function fibonacciOptimize(n) {
      if (n === 1) return 1;
      if (n === 2) return 2;
      var current = 2;
      var previous = 1;
      for (var i = 3; i <= n; i++) {
        var temp = current;
        current = previous + current;
        previous = temp;
      }
      return current;
    }
    //计算阶乘
    var res1 = fibonacci(40);
    var res2 = memorize(fibonacci)(40);
    var res3 = fibonacciOptimize(40);
    //计算出来的res3优于res2,res2优于res1
    

    总结

    运行代码的总量越大,优化带来的性能提升越明显
    正如其他编程语言,代码的写法与算法选用影响 JS 的运行时间,与其他编程语言不同,JS 可用资源有限,所以优化固然重要

    • for, while, do while 循环的性能特性相似,谁也不比谁更快或更慢
    • 除非要迭代遍历一个属性未知的对象,否则不要使用 for-in 循环
    • 改善循环的最佳方式减少每次迭代中的运算量,并减少循环迭代次数
    • 一般来说 switch 总比 if-else 更快,但总不是最好的解决方法
    • 当判断条件较多,查表法优于 if-else 和 switch
    • 浏览器的调用栈大小限制了递归算法在 js 中的应用,栈溢出导致其他代码不能正常执行
    • 如果遇到栈溢出,将方法修改为制表法,可以避免重复工作

    五、字符串和正则表达式 String And Regular Expression

    在 JS 中,正则是必不可少的东西,它的重要性远远超过烦琐的字符串处理

    字符串链接 Stirng Concatenation

    字符串连接表现出惊人的性能紧张。通常一个任务通过一个循环,向字符串末尾不断地添加内容,来创建一个字符串(例如,创建一个 HTML 表或者一个 XML 文档),但此类处理在一些浏览器上表现糟糕而遭人痛恨

    Method Example
    + str = 'a' + 'b' + 'c';
    += str = 'a'; str += 'b'; str += 'c';
    array.join() str = ['a','b','c'].join('');
    string.concat() str = 'a'; str = str.concat('b', 'c');

    当连接少量的字符串,上述的方式都很快,可根据自己的习惯使用;
    当合并字符串的长度和数量增加之后,有些函数就开始发挥其作用了

    + & +=

    str += "a" + "b";
    

    此代码执行时,发生四个步骤

    1. 内存中创建了一个临时字符串
    2. 临时字符串的值被赋予'ab'
    3. 临时串与 str 进行连接
    4. 将结果赋予 str

    下面的代码通过两个离散的表达式直接将内容附加在 str 上避免了临时字符串

    str += "a";
    str += "b";
    

    事实上用一行代码就可以解决

    str = str + "a" + "b";
    

    赋值表达式以 str 开头,一次追加一个字符串,从左至右依次连接。如果改变了连接顺序(例如:str = 'a' + str + 'b'),你会失去这种优化,这与浏览器合并字符串时分配内存的方法有关。除 IE 外,浏览器尝试扩展表达式左端字符串的内存,然后简单地将第二个字符串拷贝到它的尾部。如果在一个循环中,基本字符串在左端,可以避免多次复制一个越来越大的基本字符串。

    Array.prototype.join

    Array.prototype.join 将数组的所有元素合并成一个字符串,并在每个元素之间插入一个分隔符字符串。若传递一个空字符串,可将数组的所有元素简单的拼接起来

    var start = Date.now();
    var str = "I'm a thirty-five character string.",
      newStr = "",
      appends = 5000000;
    while (appends--) {
      newStr += str;
    }
    var time = Date.now() - start;
    console.log("耗时:" + time + "ms"); //耗时:1360ms
    var start = Date.now();
    var str = "I'm a thirty-five character string.",
      strs = [],
      newStr = "",
      appends = 5000000;
    while (appends--) {
      strs[strs.length] = str;
    }
    newStr = strs.join("");
    var time = Date.now() - start;
    console.log("耗时:" + time + "ms"); //耗时:414ms
    

    这一难以置信的改进结果是因为避免了重复的内存分配和拷贝越来越大的字符串。

    String.prototype.concat

    原生字符串连接函数接受任意数目的参数,并将每一个参数都追加在调用函数的字符串上

    var str = str.concat(s1);
    var str = str.concat(s1, s2, s3);
    var str = String.prototype.concat.apply(str, array);
    

    大多数情况下 concat 比简单的+或+=慢一些

    Regular Expression Optimization 正则表达式优化

    许多因素影响正则表达式的效率,首先,正则适配的文本千差万别,部分匹配时比完全不匹配所用的时间要长,每种浏览器的正则引擎也有不同的内部优化

    正则表达式工作原理

    1. 编译
      当你创建了一个正则表达式对象之后(使用一个正则表达式直接量或者 RegExp 构造器),浏览器检查你的模板有没有错误,然后将它转换成一个本机代码例程,用执行匹配工作。如果你将正则表达式赋给一个变量,你可以避免重复执行此步骤。
    2. 设置起始位置
      当一个正则表达式投入使用时,首先要确定目标字符串中开始搜索的位置。它是字符串的起始位置,或者由正则表达式的 lastIndex 属性指定,但是当它从第四步返回到这里的时候(因为尝试匹配失败),此位置将位于最后一次尝试起始位置推后一个字符的位置上
    3. 匹配每个正则表达式的字元
      正则表达式一旦找好起始位置,它将一个一个地扫描目标文本和正则表达式模板。当一个特定字元匹配失败时,正则表达式将试图回溯到扫描之前的位置上,然后进入正则表达式其他可能的路径上
    4. 匹配成功或失败
      如果在字符串的当前位置上发现一个完全匹配,那么正则表达式宣布成功。如果正则表达式的所有可能路径都尝试过了,但是没有成功地匹配,那么正则表达式引擎回到第二步,从字符串的下一个字符重新尝试。只有字符串中的每个字符(以及最后一个字符后面的位置)都经历了这样的过程之后,还没有成功匹配,那么正则表达式就宣布彻底失败。

    理解回溯

    在大多数现代正则表达式实现中(包括 JavaScript 所需的),回溯是匹配过程的基本组成部分。它很大程度上也是正则表达式如此美好和强大的根源。然而,回溯计算代价昂贵,如果你不够小心的话容易失控。虽然回溯是整体性能的唯一因素,理解它的工作原理,以及如何减少使用频率,可能是编写高效正则表达式最重要的关键点。

    正则表达式匹配过程

    • 当一个正则表达式扫描目标字符串时,它从左到右逐个扫描正则表达式的组成部分,在每个位置上测试能不能找到一个匹配。对于每一个量词和分支,都必须决定如何继续进行。如果是一个量词(诸如*,+?,或者{2,}),正则表达式必须决定何时尝试匹配更多的字符;如果遇到分支(通过|操作符),它必须从这些选项中选择一个进行尝试。
    • 每当正则表达式做出这样的决定,如果有必要的话,它会记住另一个选项,以备将来返回后使用。如果所选方案匹配成功,正则表达式将继续扫描正则表达式模板,如果其余部分匹配也成功了,那么匹配就结束了。但是如果所选择的方案未能发现相应匹配,或者后来的匹配也失败了,正则表达式将回溯到最后一个决策点,然后在剩余的选项中选择一个。它继续这样下去,直到找到一个匹配,或者量词和分支选项的所有可能的排列组合都尝试失败了,那么它将放弃这一过程,然后移动到此过程开始位置的下一个字符上,重复此过程。

    示例分析

    /h(ello|appy) hippo/.test("hello there, happy hippo");
    

    此正则表达式匹配“hello hippo”或“happy hippo”。测试一开始,它要查找一个 h,目标字符串的第一个字母恰好就是 h,它立刻就被找到了。接下来,子表达式(ello|appy)提供了两个处理选项。正则表达式选择最左边的选项(分支选择总是从左到右进行),检查 ello 是否匹配字符串的下一个字符。确实匹配,然后正则表达式又匹配了后面的空格。然而在这一点上它走进了死胡同,因为 hippo 中的 h 不能匹配字符串中的下一个字母 t。此时正则表达式还不能放弃,因为它还没有尝试过所有的选择,随后它回溯到最后一个检查点(在它匹配了首字母 h 之后的那个位置上)并尝试匹配第二个分支选项。但是没有成功,而且也没有更多的选项了,所以正则表达式认为从字符串的第一个字符开始匹配是不能成功的,因此它从第二个字符开始,重新进行查找。它没有找到 h,所以就继续向后找,直到第 14 个字母才找到,它匹配 happy 的那个 h。然后它再次进入分支过程。这次 ello 未能匹配,但是回溯之后第二次分支过程中,它匹配了整个字符串“happy hippo”(如图 5-4)。匹配成功了。

    回溯失控

    当一个正则表达式占用浏览器上秒,上分钟或者更长时间时,问题原因很可能是回溯失控。正则表达式处理慢往往是因为匹配失败过程慢,而不是匹配成功过程慢。

    var reg = /<html>[\s\S]*?<head>[\s\S]*?<title>[\s\S]*?<\/title>[\s\S]*?<\/head>[\s\S]*?<body>[\s\S]*?<\/body>[\s\S]*?<\/html>/;
    //优化如下
    var regOptimize = /<html>(?=([\s\S]*?<head>))\1(?=([\s\S]*?<title>))\2(?=([\s\S]*?<\/title>))\3(?=([\s\S]*?<\/head>))\4(?=([\s\S]*?<body>))\5(?=([\s\S]*?<\/body>))\6[\s\S]*?<\/html>/;
    

    现在如果没有尾随的那么最后一个[\s\S]*?将扩展至字符串结束,正则表达式将立刻失败因为没有回溯点可以返回

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

    • 关注如何让匹配更快失败
    • 正则表达式以简单的,必需的字元开始
    • 编写量词模板,使它们后面的字元互相排斥
    • 减少分支的数量,缩小它们的范围
    • 使用非捕获组
    • 捕获感兴趣的文字,减少后处理
    • 暴露所需的字元
    • 使用适当的量词
    • 将正则表达式赋给变量,以重用它们
    • 将复杂的正则表达式拆分为简单的片断

    什么时候不应该使用正则表达式

    var endsWithSemicolon = /;$/.test(str);
    

    你可能觉得很奇怪,虽说当前没有哪个浏览器聪明到这个程度,能够意识到这个正则表达式只能匹配字符串的末尾。最终它们所做的将是一个一个地测试了整个字符串。字符串的长度越长(包含的分号越多),它占用的时间也越长

    var endsWithSemicolon = str.charAt(str.length - 1) == ";";
    

    这种情况下,更好的办法是跳过正则表达式所需的所有中间步骤,简单地检查最后一个字符是不是分号:

    这个例子使用 charAt 函数在特定位置上读取字符。字符串函数 slice,substr,和 substring 可用于在特定位置上提取并检查字符串的值

    所有这些字符串操作函数速度都很快,当您搜索那些不依赖正则表达式复杂特性的文本字符串时,它们有助于您避免正则表达式带来的性能开销

    字符串修剪

    正则表达式允许你用很少的代码实现一个修剪函数,这对 JavaScript 关心文件大小的库来说十分重要。可能最好的全面解决方案是使用两个子表达式:一个用于去除头部空格,另一个用于去除尾部空格。这样处理简单而迅速,特别是处理长字符串时。

    //方法 用正则表达式修剪
    // trim1
    String.prototype.trim = function() {
      return this.replace(/^\s+/, "").replace(/\s+$/, "");
    };
    //trim2
    String.prototype.trim = function() {
      return this.replace(/^\s+|\s+$/g, "");
    };
    // trim 3
    String.prototype.trim = function() {
      return this.replace(/^\s*([\s\S]*?)\s*$/, "$1");
    };
    // trim 4
    String.prototype.trim = function() {
      return this.replace(/^\s*([\s\S]*\S)?\s*$/, "$1");
    };
    // trim 5
    String.prototype.trim = function() {
      return this.replace(/^\s*(\S*(\s+\S+)*)\s*$/, "$1");
    };
    //方法二 不使用正则表达式修剪
    String.prototype.trim = function() {
      var start = 0;
      var end = this.length - 1;
      //ws 变量包括 ECMAScript 5 中定义的所有空白字符
      var ws =
        "\n\r\t\f\x0b\xa0\u1680\u180e\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u200b\u2028\u2029\u202f\u205f\u3000\ufeff";
      while (ws.indexOf(this.charAt(start)) > -1) {
        start++;
      }
      while (end > start && ws.indexOf(this.charAt(end)) > -1) {
        end--;
      }
      return this.slice(start, end + 1);
    };
    //方法三 混合解决方案
    String.prototype.trim = function() {
      var str = this.replace(/^\s+/, ""),
        end = str.length - 1,
        ws = /\s/;
      while (ws.test(str.charAt(end))) {
        end--;
      }
      return str.slice(0, end + 1);
    };
    

    简单地使用两个子正则表达式在所有浏览器上处理不同内容和长度的字符串时,均表现出稳定的性能。因此它可以说是最全面的解决方案。混合解决方案在处理长字符串时特别快,其代价是代码稍长,在某些浏览器上处理尾部长空格时存在弱点

    总结

    • 使用简单的+和+=取代数组联合,可避免(产生)不必要的中间字符串
    • 当连接数量巨大或尺寸巨大的字符串时,使用数组联合
    • 使相邻字元互斥,避免嵌套量词对一个字符串的相同部分多次匹配,通过重复利用前瞻操作的原子特性去除不必要的回溯

    六、响应接口

    用户倾向于重复尝试这些不发生明显变化的动作,所以确保网页应用程序的响应速度也是一个重要的性能关注点

    浏览器 UI 线程

    JavaScript 和 UI 更新共享的进程通常被称作浏览器 UI 线程, UI 线程围绕着一个简单的队列系统工作,任务被保存到队列中直至进程空闲。一旦空闲,队列中的下一个任务将被检索和运行。这些任务不是运行 JavaScript 代码,就是执行 UI 更新,包括重绘和重排版.
    大多数浏览器在 JavaScript 运行时停止 UI 线程队列中的任务,也就是说 JavaScript 任务必须尽快结束,以免对用户体验造成不良影响

    Brendan Eich,JavaScript 的创造者,引用他的话说,“[JavaScript]运行了整整几秒钟很可能是做错了什么……”

    定时器基础

    定时器与 UI 线程交互的方式有助于分解长运行脚本成为较短的片断

    定时器精度

    所有浏览器试图尽可能准确,但通常会发生几毫秒滑移,或快或慢。正因为这个原因,定时器不可用于测量实际时间

    总结

    • JavaScript 运行时间不应该超过 100 毫秒。过长的运行时间导致 UI 更新出现可察觉的延迟,从而对整体用户体验产生负面影响
    • JavaScript 运行期间,浏览器响应用户交互的行为存在差异。无论如何,JavaScript 长时间运行将导致用户体验混乱和脱节。
    • 同一时间只有一个定时器存在,只有当这个定时器结束时才创建一个新的定时器。以这种方式使用定时器不会带来性能问题
    • 定时器可用于安排代码推迟执行,它使得你可以将长运行脚本分解成一系列较小的任务

    七、Ajax

    目前最常用的方法中,XMLHttpRequest(XHR)用来异步收发数据。所有现代浏览器都能够很好地支持它,而且能够精细地控制发送请求和数据接收。你可以向请求报文中添加任意的头信息和参数(包括 GET 和 POST),并读取从服务器返回的头信息,以及响应文本自身

    请求数据

    五种常用技术用于向服务器请求数据

    • XMLHttpRequest (XHR)
    • Dynamic script tag insertion 动态脚本标签插入
    • iframes
    • Comet
    • Multipart XHR 多部分的 XHR

    XMLHttpRequest

    //封装ajax
    var xhr = new XMLHttpRequest();
    xhr.onreadystatechange = function() {
      if (xhr.readyState === 4 && xhr.status >= 200) {
        //
      }
    };
    xhr.open(type, url, true);
    xhr.setRequestHeader("Content-Type", contentType);
    xhr.send(null);
    

    动态脚本标签插入

    发送数据

    • XMLHttpRequest
    • 图像灯标

    数据格式

    通过 Douglas Crockford 的发明与推广,JSON 是一个轻量级并易于解析的数据格式,它按照 JavaScript 对象和数组字面语法所编写

    Ajax 性能向导

    数据传输技术和数据格式

    • 缓存数据
    • 设置 HTTP 头
    • 本地存储数据

    总结

    高性能 Ajax 包括:知道你项目的具体需求,选择正确的数据格式和与之相配的传输技术

    • 减少请求数量,可合并 js 和 css 文件
    • 缩短页面的加载时间,在页面其它内容加载之后,使用 Ajax 获取少量重要文件
    • JSON 是高性能 AJAX 的基础,尤其在使用动态脚本注入时
    • 学会何时使用一个健壮的 Ajax 库,何时编写自己的底层 Ajax 代码

    封装自己的 ajax 库

    (function(root) {
      root.MyAjax = (config = {}) => {
        let url = config.url;
        let type = config.type || "GET";
        let async = config.async || true;
        let headers = config.headers || [];
        let contentType = config.contentType || "application/json;charset=utf-8";
        let data = config.data;
        let dataType = config.dataType || "json";
        let successFn = config.success;
        let errorFn = config.error;
        let completeFn = config.complete;
        let xhr;
        if (window.XMLHttpRequest) {
          xhr = new XMLHttpRequest();
        } else {
          xhr = new ActiveXObject("Microsoft.XMLHTTP");
        }
        xhr.onreadystatechange = () => {
          if (xhr.readyState === 4) {
            if (xhr.status === 200) {
              let rsp = xhr.responseText || xhr.responseXML;
              if (dataType === "json") {
                rsp = eval("(" + rsp + ")");
              }
              successFn(rsp, xhr.statusText, xhr);
            } else {
              errorFn(xhr.statusText, xhr);
            }
            if (completeFn) {
              completeFn(xhr.statusText, xhr);
            }
          }
        };
        xhr.open(type, url, async);
        //设置超时
        if (async) {
          xhr.timeout = config.timeout || 0;
        }
        //设置请求头
        for (let i = 0; i < headers.length; ++i) {
          xhr.setRequestHeader(headers[i].name, headers[i].value);
        }
        xhr.setRequestHeader("Content-Type", contentType);
        //send
        if (
          typeof data == "object" &&
          contentType === "application/x-www-form-urlencoded"
        ) {
          let s = "";
          for (attr in data) {
            s += attr + "=" + data[attr] + "&";
          }
          if (s) {
            s = s.slice(0, s.length - 1);
          }
          xhr.send(s);
        } else {
          xhr.send(data);
        }
      };
    })(window);
    

    八、编程实践

    • 避免二次评估,比如 eval,Function

    • 使用对象/数组直接量

    • 不要重复工作

    • 延迟加载

    • 条件预加载

    • 使用速度快的部分

    • 位操作运算符

      四种位逻辑操作符

      • 位与
        比如判断数奇偶
      num % 2 === 0; //取模与0进行判断
      num & 1; //位与1结果位1则为奇数,为0则为偶数
      
      • 位或
      • 位异或
      • 位非
    • 位掩码
      位掩码在计算机科学中是一种常用的技术,可同时判断多个布尔 选项,快速地将数字转换为布尔标志数组。掩码中每个选项的值都等于 2 的幂

    var OPTION_A = 1;
    var OPTION_B = 2;
    var OPTION_C = 4;
    var OPTION_D = 8;
    var OPTION_E = 16;
    

    通过定义这些选项,你可以用位或操作创建一个数字来包含多个选项:

    var options = OPTION_A | OPTION_C | OPTION_D;
    

    可以使用位与操作检查一个给定的选项是否可用

    //is option A in the list?
    if (options & OPTION_A) {
      //do something
    }
    //is option B in the list?
    if (options & OPTION_B) {
      //do something
    }
    

    像这样的位掩码操作非常快,正因为前面提到的原因,操作发生在系统底层。如果许多选项保存在一起并经常检查,位掩码有助于加快整体性能

    原生方法

    无论你怎样优化 JavaScript 代码,它永远不会比 JavaScript 引擎提供的原生方法更快。经验不足的 JavaScript 开发者经常犯的一个错误是在代码中进行复杂的数学运算,而没有使用内置 Math 对象中那些性能更好的版本。Math 对象包含专门设计的属性和方法,使数学运算更容易。

    //查看Math对象所有方法
    Object.getOwnPropertyNames(Math);
    

    总结

    • 通过避免使用 eval()和 Function()构造器避免二次评估。此外,给 setTimeout()和 setInterval()传递函数参数而不是字符串参数。
    • 创建新对象和数组时使用对象直接量和数组直接量。它们比非直接量形式创建和初始化更快。
    • 避免重复进行相同工作。当需要检测浏览器时,使用延迟加载或条件预加载
    • 当执行数学远算时,考虑使用位操作,它直接在数字底层进行操作。
    • 原生方法总是比 JavaScript 写的东西要快。尽量使用原生方法

    九、创建并部署高性能 JavaScript 应用程序

    • 合并 js 文件,减少 HTTP 请求的数量
    • 以压缩形式提供 js 文件(gzip 编码)
    • 通过设置 HTTP 响应报文头使 js 文件可缓存,通过向文件名附加时间戳解决缓存问题
    • 使用CDN提供 js 文件,CDN 不仅可以提高性能,它还可以为你管理压缩和缓存

    十、工具

    当网页或应用程序变慢时,分析网上传来的资源,分析脚本的运行性能,使你能够集中精力在那些需要努力优化的地方。

    • 使用网络分析器找出加载脚本和其它页面资源的瓶颈所在,这有助于决定哪些脚本需要延迟加载,或者进行进一步分析
    • 尽量延迟加载脚本以使页面渲染速度更快,向用户提供更好的整体体验。
    • 使用性能分析器找出脚本运行时速度慢的部分,检查每个函数所花费的时间,以及函数被调用的次数,通过调用栈自身提供的一些线索来找出哪些地方应当努力优化

    后记

    能读到最后的同学也不容易,毕竟篇幅稍长。本书大概花了三周的零碎时间读完,建议大家读一读。如果大家在看书过程中存在疑问,不妨打开电脑验证书中作者的言论,或许会更加深刻

    前端性能优化之 JavaScript

    好书推荐、视频分享,公众号"读书ReadBook"与您一起进步

    相关文章

      网友评论

        本文标题:前端性能优化之 JavaScript

        本文链接:https://www.haomeiwen.com/subject/qnbtqctx.html