vlambda博客
学习文章列表

​教你写出高性能 JavaScript

教你写出高性能 JavaScript

最近又在研究性能优化,于是复习起了三年前自己看完高性能 JavaScript 那本书整理的笔记,觉得虽然三年前整理的东西了,但是现在拿出来还是觉得不过时很有用。本文主要介绍两大块「页面级优化」、「代码级优化」;至于实践与应用还有工具简单介绍且上篇文章有讲解过:

页面级优化

​教你写出高性能 JavaScript
image-20210219170224492

页面级优化-加载和执行

「从设计实现层面简化页面」

  1. 「脚本位置:底部」将脚本内容在页面信息内容加载后再加载

    将脚本尽可能的往后挪,减少对并发下载的影响。

  2. 「组织脚本:减少外链」

    1. 合理设置 HTTP 缓存 缓存的力量是强大的,恰当的缓存设置可以大大的减少 HTTP 请求。

      能缓存越多越好,能缓存越久越好。尽可能的让资源能够在缓存中待得更久。

      eg.很少变化的图片资源可以直接通过 HTTP Header 中的 Expires 设置一个很长的过期头 ;

      变化不频繁而又可能会变的资源可以使用 Last-Modifed 来做请求验证

    2. 资源合并与压缩 尽可能的将外部的脚本、样式进行合并,多个合为一个。

      CSS、 Javascript、Image 都可以用相应的工具进行压缩,压缩后往往能省下不少空间

      推荐工具:grunt 、gulp、 http://tool.css-js.com/( UglifyJS )、 webpack

    3. CSS Sprites 合并 CSS 图片,减少请求数

    4. Inline Images 将图片嵌入到页面或 CSS 中

    5. Lazy Load Images

      图片懒加载

  3. 「无阻塞脚本:页面加载完之后再加载 js 代码」window 对象的 load 时间触发之后再下载脚本

    (1)延迟的脚本:并行下载,下载过程中不会产生阻塞 defer(IE4+和 firefox3.5+)等待页面完成后执行

    async(HTML5 异步加载脚本)加载完成后自动执行

    eg. https://github.com/dingxiaoxue/shareDemo/blob/master/defer.html

    (2)动态脚本元素:

    凭借着它在跨浏览器兼容性和易用性的优势,成为最通用的无阻塞加载解决方案 eg. https://github.com/dingxiaoxue/shareDemo/blob/master/loadJs.html

    注意加载顺序

    Eg.https://github.com/dingxiaoxue/shareDemo/blob/master/loadJs.html

    由于这个过程是异步的,因此文件大一点不会有影响

    (3) XMLHttpRequest 脚本注入:无阻塞加载脚本的另一种方法

    优点:同一套代码适合所有浏览器、下载后不会立即执行可推迟到你准备好的时候

    缺点:局限性 js 文件必须与所有请求的页面处于同一个域(不适合咱们从 CDN 下载)

    (4)推荐无阻塞模式:

    A.上面的(2) B.YUI3 https://github.com/yui/yui3/ C.LazyLoad 类库 D.LABjs

页面级优化-总结

管理浏览器中的 JavaScript 代码是棘手的问题,因为代码执行过程会阻塞浏览器的其他进程(如用户界面绘制)每次遇到 script 标签,页面都必须停下来等待代码下载并执行,然后继续处理其他部分。尽管如此,还是有几种方法减少 js 对性能的影响:

  1. body 闭合标签之前,将所有的 script 标签放在页面底部。这样能确保在脚本执行之前页面已经完成渲染
  2. 合并脚本。页面中的 script 标签越少加载的也就越快,响应也越迅速。
  3. 多种无阻塞下载 js 的方法:使用
 <script defer>

defer 属性

使用动态创建的 script 元素来下载并执行代码

使用 XHR 对象下载 js 代码并注入页面

通过以上策略,可以大大提高那些需要大量使用大量 js 的 web 应用的实际性能

代码级优化

​教你写出高性能 JavaScript
image-20210219170255140

代码级优化-数据存取

  1. 「管理作用域:」(1)作用域链和标识符解析

    多次调用同一个函数就会导致创建多个执行环境,函数执行完毕后执行环境被销毁

    函数调用过程中每个标识符都要经历一个搜索的过程会影响性能

    (2)标识符解析的性能

    一个标识符的所在位置越深,它的读写速度也就越慢;

    在一个没有优化的 js 引擎浏览器中,建议尽可能使用局部变量

    先将一个全局变量的引用存储在一个局部变量中,然后使用这个局部变量代替全局变量

    eg. https://github.com/dingxiaoxue/shareDemo/blob/master/identifie.html

    (3)改变作用域链

    With()

    Try{method()}catch(ex){handleError(ex)//委托给错误处理函数}

    Eval()

    尽量避免使用,确实有必要时候再用动态作用域

    (4)闭包、作用域和内存

    闭包比非闭包小号更多的内存,IE 使用非原生的 js 对象来实现 DOM 对象,因此会导致内存泄露

    频繁访问跨作用域的标识符时,每次访问都会带来性能损失

    跨作用域的处理建议,减轻闭包对执行速度的影响

    将常用的跨作用域变量存储在局部变量中,然后直接访问局部变量

  2. 「对象成员(属性和方法)」(1)原型 prototype

    hasOwnProperty() 判断对象是否包含特定的实例成员

    In 确定对象是否包含特定的属性(搜索实例和原型)

    (2)原型链 prototype chains

    「减少作用域链查找」

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

    (3)嵌套成员 每次遇见点操作符,嵌套成员会导致 js 搜索引擎搜索所有的对象成员

    对象成员嵌套的越深,读取速度越慢

    Location.href 最快

    Window.location.href

    window.loacation.href.toString() 最慢

    (4)缓存对象成员值

    不要在同一个函数里多次查找同一个对象成员,除非他的值改变了

  1. 「属性或方法在原型链中位置越深,访问他的速度也越慢」

    字面量:只代表自身,不存储在特定位置。

    字符串、数字、布尔值、对象、数组、函数、正则表达式、null、undefined

    本地变量:开发人员使用关键字 var 定义的数据存储单元

    数组元素:存储在 JavaScript 数组对象内部,以数字作为索引

    对象成员:存储在 JavaScript 对象内部,以字符串作为索引

    不同浏览器分别对四种数据存储位置进行 200000 次操作所用时间

    ​教你写出高性能 JavaScript
    image-20210219171539192

    「总结数据存取」

    在 JavaScript 中,数据存储的位置会对代码整体性能产生重大影响。

    数据存储共有四种方式:字面量、变量、数组项、对象成员。

    通过以上策略,可以显著提升那些需要使用大量变量 JavaScript 的 Web 应用的实际性能

    1. 访问「字面量和局部变量的速度最快」,相反,访问数组元素和对象成员相对较慢

    2. 由于局部变量存在于作用域链中的其实位置,因此,访问局部变量比访问跨作用域变量更快

      变量在作用域中的位置越深,访问所需要的时间越长。

      由于全局变量总处于作用域的最末端,因此访问速度也最慢

    3. 「避免用 with 语句」,因为他会改变执行环境作用域链。try-catch 语句中的 catch 子句也具有同样的影响 因此要小心使用

    4. 「嵌套的对象成员」会明显影响性能,尽量「少用」

    5. 属性或方法在原型链中位置越深,访问他的速度也越慢

    6. 通常来说,你可以把常用的「对象成员、数组元素、跨域变量保存局部变量中来改善 JavaScript 性能」,因为局部变量更快 a. 对任何对象属性的访问超过 1 次

      b. 对任何数组成员的访问次数超过 1 次

代码级优化-DOM 编程

代码级优化-DOM 访问与修改

DOM 的访问与修改

「最小化 DOM 访问次数,尽可能在 js 端处理。」

「如果需要多次访问某个 DOM 节点,请使用局部变量存储他的引用。」

访问 DOM 的次数越多,代码的运行速度越慢

减少访问 DOM 的次数,把运算尽量留在 ECMAScript 这一端

eg.https://github.com/dingxiaoxue/shareDemo/blob/master/domChange.html(155倍)

​教你写出高性能 JavaScript
image-20210219172609035
  1. 「innerHTML 对比 DOM 方法」document.creatElement() VS innerHTML

    除了 webkit 内核(chrome 和 Safari)外的所有浏览器,innerHTML 会更快一些

    eg. https://github.com/dingxiaoxue/shareDemo/blob/master/innerHTMLVsDom.html

    eg. https://github.com/dingxiaoxue/shareDemo/blob/master/innerHTMLVsDOM2.html

    对比使用 innerHTML 和 DOM 方法来创建 1000 行表格;在 IE6 中,innerHTML 要快 3 倍以上,而在新版 webkit 浏览器中 innerHTML 则会较慢。​教你写出高性能 JavaScript

  2. 「节点克隆(稍快一点)」克隆已有元素——使用 element.cloneNode()替代 document.creatElement()

    Eg.https://github.com/dingxiaoxue/shareDemo/blob/master/cloneDOM.html

  3. 「HTML 集合」小心处理 HTML 集合,因为它实时联系着底层文档。把集合的长度缓存到一个变量中,并在迭代中使用它。如果需要经常操作集合,建议把他拷贝到一个数组中。

(1)昂贵的集合 在相同的内容和数量下,遍历一个数组的速度明显比遍历一个 HTML 集合快

优化方法:把集合的长度缓存到一个局部变量中,然后在循环的条件退出语句中使用该变量

Eg. https://github.com/dingxiaoxue/shareDemo/blob/master/jihe.html

toArray() 集合转数组函数

(2)访问集合时使用局部变量

在循环中使用局部变量存储集合引用和集合元素更快

eg. https://github.com/dingxiaoxue/shareDemo/blob/master/variableCom.html

​教你写出高性能 JavaScript
image-20210219173022626
  1. 「遍历 DOM」如果可能得话,使用速度更快的 API。

 getElementById()    getElementsByTagName()
 querySelectorAll()   querySelectorAll() //更快
 eg.var elements = document.querySelectorAll('#menu a');
 var elements = document.getElementById('menu').getElementsByTagName('a');
 var errs = document.querySelectorAll('div.warning, div.notice');

(1)获取 DOM 元素

childNodes VS 「nextSilbing」 (老版本的 IE 中 nextSiblings 更快 7 中 105 倍 6 中 16 倍)

Eg.https://github.com/dingxiaoxue/shareDemo/blob/master/nextSibling.html

(2)元素节点

在所有浏览器中,「children 都比 childNodes 要快」(1.5-3 倍)

IE 中遍历 children 集合的速度明显快于遍历 childNode(IE6 24 倍,IE7 124 倍)

eg.https://github.com/dingxiaoxue/shareDemo/blob/master/variableCom.html

(3)选择器 API

代码级优化- 重绘与重排

当浏览器下载完所有页面 HTML 标记,JavaScript,CSS,图片之后,它解析文件并创建两个内部数据结构:

「一棵树」:表示页面结构(当布局和几何改变时需要重排版)

「一棵渲染树」 :表示 DOM 节点如何显示

「1. 重排何时发生」

(1)添加或删除可见的 DOM 元素

(2)元素位置改变

(3)元素尺寸改变(因为边距,填充,边框宽度,宽度,高度等属性改变)

(4)内容改变,例如,文本改变或图片被另一个不同尺寸的所替代

(5)最初的页面渲染

(6)浏览器窗口改变尺寸

根据改变的性质,渲染树上或大或小的一部分需要重新计算。某些改变可导致重排版整个页面:例如,当一个滚动条出现时。

获取布局信息的操作将导致刷新队列动作,最好不要使用以下属性:

• offsetTop, offsetLeft, offsetWidth, offsetHeight • scrollTop, scrollLeft, scrollWidth, scrollHeight • clientTop, clientLeft, clientWidth, clientHeight • getComputedStyle()(currentStylein IE)(在 IE 中此函数称为 currentStyle)

「2. 最小化重绘和重排」

批量改变样式时,“离线”操作 DOM 树,使用缓存,并减少访问布局信息次数

(1)改变样式

(2)批量修改 DOM

当你需要对 DOM 元素进行多次修改时,你可以通过以下步骤减少重绘和重排版的次数:

从文档流中摘除该元素

对其应用多重改变

元素带回文档中

三种基本方法可以将 DOM 从文档中摘除:

隐藏元素,进行修改,然后再显示它

使用一个文档片断在已存 DOM 之外创建一个子树,然后将它拷贝到文档中。

将原始元素拷贝到一个脱离文档的节点中,修改副本,然后覆盖原始元素。

「3.让元素脱离动画流」

使用绝对坐标定位页面动画的元素,使它位于页面布局流之外。

启动元素动画。当它扩大时,它临时覆盖部分页面。这是一个重绘过程,但只影响页面的一小部分,避免重排版并重绘一大块页面。

当动画结束时,重新定位,从而只一次下移文档其他元素的位置。

「4.事件委托」

使用事件委托来减少事件处理器的数量

「5.:hover」少用

代码级优化- 算法和流程控制

代码整体结构是影响运行速度的主要因素之一。

代码的组织结构和解决具体问题的思路是影响代码性能的主要因素。

  1. 「循环」在大多数变成语言中,代码执行时间大部分消耗在循环中。

    死循环或长时间运行的循环会严重影响用户体验。

    「循环类型(4 种)」

    For(){//循环主体}

    While(){(){//循环主体}

    Do{{//循环主体}while();

    For( var prop in object){//循环主体};

    「循环性能」

    不断引发循环性能争论的源头是循环类型的选择。

    「Js 中 for-in 循环比其他几种明显要慢」,另外三种速度区别不大

    迭代一个属性未知的对象——for-in(不要用 for-in 遍历数组成员)

    遍历一个数量有限的已知属性列表——1/2/3(循环类型的选择要基于需求,而不是性能)

    A.每次迭代处理事务

    B.迭代次数

    通过减少 A、B 这两者中的一个或者全部的时间开销,你就提升循环的整体性能。

    减少迭代的工作量

    限制循环中耗时操作的数量

    有一点需要注意的是,javascript 没有块级作用域,只有函数级作用域,也就是说在 for 循环初始化中的 var 语句会创建一个函数级变量而非循环级变量

    「优化循环的方法有如下」

    1、减少对象成员及数组项的查找次数(使用局部变量保存需要查找的对象成员)

    2、「颠倒数组的顺序来提高循环性能」,也就是从最后一项开始向前处理

     for (var i = arr.length-1; i >= 0 ; i--) {
                //process
            }

    3、相信大家都会尽可能的使用 for 循环而非 jQuery 的 each 来遍历数组,那是因为 jQuery 的 each 方法是基于函数的迭代。尽管基于函数的迭代提供了一个更为便利的迭代方法,但它比基于循环的迭代在慢许多。

    4、有时候我们会想到底是使用 if-else 呢还是使用 switch,「事实上在大多数情况下 switch 比 if-else 运行得要快,所以当判断多于两个离散值时,switch 语句是更佳的选择」

    5、优化 if-else 最简单的方法就是「确保最可能出现的条件放在首位」,另外一个方法就是优化条件判断的次数,看下面的代码您就懂了

 if (value == 0) {
   return result0;
 } else if (value == 1) {
   return result1;
 } else if (value == 2) {
   return result2;
 } else if (value == 3) {
   return result3;
 } else if (value == 4) {
   return result4;
 } else if (value == 5) {
   return result5;
 } else if (value == 6) {
   return result6;
 } else if (value == 7) {
   return result7;
 } else if (value == 8) {
   return result8;
 } else if (value == 9) {
   return result9;
 } else if (value == 10) {
   return result10;
 }

下面这种方法就是使用二分搜索法将值域分成一系列区间,然后逐步缩小区范围,对上面的例子进行的优化

 if (value < 6) {
   if (value < 3) {
     if (value == 0) {
       return result0;
     } else if (value == 1) {
       return result1;
     } else {
       return result2;
     }
   } else {
     if (value == 3) {
       return result3;
     } else if (value == 4) {
       return result4;
     } else {
       return result5;
     }
   }
 } else {
   if (value < 8) {
     if (value == 6) {
       return result06;
     } else if (value == 7) {
       return result7;
     }
   } else {
     if (value == 8) {
       return result8;
     } else if (value == 9) {
       return result9;
     } else {
       return result10;
     }
   }
 }

6、使用递归虽然可以把复杂的算法变得简单,但递归函数如果终止条件不明确或缺少终止条件会导致函数长时间运行。所以递归函数还可能会遇到浏览器“调用栈大小限制”

使用优化后的循环来替代长时间运行的递归函数可以提升性能,因为运行一个循环比反复调用一个函数的开销要少的多

如果循环资料太多,可以考虑使用如下介绍的达夫设备原理来提升性能

  1. 「达夫设备」


     var iterations = Math.floor(items.length / 8),
         startAt = items.length % 8,
         i = 0;
    do {
      //每次循环最多可调用8次process
      switch (startAt) {
        case 0: process(items[i++]);
        case 7: process(items[i++]);
        case 6: process(items[i++]);
        case 5: process(items[i++]);
        case 4: process(items[i++]);
        case 3: process(items[i++]);
        case 2: process(items[i++]);
        case 1: process(items[i++]);
      }
      startAt = 0;
    } while (--iterations);
    ```

 2. 
   var i = items.length % 8;
while (i) {
process(items[i--]);
}
i = Math.floor(items.length / 8);
while (i) {
process(items[i--]);
process(items[i--]);
process(items[i--]);
process(items[i--]);
process(items[i--]);
process(items[i--]);
process(items[i--]);
process(items[i--]);
}
```
  1. Memoization

    避免重复是 Memoization 的核心思想,它缓存前一次计算结果供后续使用,下面代码就是利用缓存结果的思想计算阶乘的

 function memFactorial(n) {
   if (!memFactorial.cache) {
     memFactorial.cache = {
       "0": 1,
       "1": 1
     };
   }
   if (!memFactorial.cache.hasOwnProperty(n)) {
     memFactorial.cache[n] = n * memFactorial(n - 1);
   }
   return memFactorial.cache[n];
 }

写成通用方法如下代码所示:

function memoize(fundamental, cache) {
  cache = cache || {};
  var shell = function (arg) {
    if (!cache.hasOwnProperty(arg)) {
      cache[arg] = fundamental(arg);
    }
    return cache[arg];
  }
  return shell;
}
//下面是调用示例
function factorial(n) {
  if (n==0) {
    return 1;
  }else{
    return n*factorial(n-1);
  }
}
var memfactorial = memoize(factorial, { "0": 1, "1": 1 });
memfactorial(6);
  1. 「算法和流程控制小结」

    1. 四种循环中(for、for-in、while、do-while),只有 for-in 循环比其它几种明显要慢,另外三种速度区别不大
    2. 除非你要迭代一个属性未知的对象,否则不要使用 for-in 循环
    3. 改善循环性能的最好办法是减少每次迭代中的运算量,并减少循环迭代的次数
    4. 一般来说,switch 总是比 if-else 更快,但不总是最好的解决方法;当判断条件较多时,查表法比 if-else 或者 switch 更快
    5. 浏览器的调用栈尺寸限制了地柜算法 JavaScript 中的应用;栈溢出错误导致其他代码也不能正常执行;如果你遇到一个栈溢出错误,将方法修改为一个迭代算法或使用制表法可以避免重复工作

代码级优化- 字符串和正则表达式

str += "one" + "two";
//以下代码分别用两行语句直接附加内容给str,从而避免产生临时字符串 性能比上面提升10%到40%;
str += "one";
str += "two";
//同样你可以用如下一句达到上面同样的性能提升
str = str + "one" + "two";
//事实上 str = str + "one" + "two";等价于 str = ((str + "one") + "two");

或许大家都喜欢用 Array.prototype.join 方法将数组中所有元素合并成一个字符串,虽然它是在 IE7 及更早版本浏览器 中合并大量字符串唯一高效的途径,「但是事实上在现代大多数浏览器中,数组项连接比其它字符串连接的方法更慢。」

在大多数情况下,使用 concat 比使用简单的+和+=要稍慢些。

当连接数量巨大或尺寸巨大的字符串时,数组联合是连接字符串最慢的方法之一。使用简单的+和+=取而代之,可避免不必要的中间字符串

代码级优化- 快速响应用的户界面

​教你写出高性能 JavaScript
image-20210220155832309

代码级优化- 数据传输 Ajax

数据传输

有 5 种常用技术用于向服务器请求数据:

  1. XMLHttpRquest(XHR)
  2. Dynamic script tag insertion(动态脚本标签插入)
  3. iframes
  4. Comet (一种 hack 技术)
  5. Multipart XHR(多部分的 XHR)

在现代高性能 JavaScript 中使用的三种技术是 XHR,动态脚本标签插入和多部分的 XHR。使用 Comet 和 iframe(作为数据传输技术)往往是极限情况,不在这里讨论。

「1.XMLHttpRquest」:允许异步发送和接收数据,可以在请求中添加任何头信息和参数,并读取服务器返回的所有头信息及响应文本

使用 XHR 时,POST 和 GET 的对比,对于那些不会改变服务器状态,只会获取数据(这种称作幂等行为)的请求,应该使用 GET,「经 GET 请求的数据会被缓存起来」,如果需要多次请求同一数据的时候它会有助于提升性能 。

只有当请求的 URL 加上参数的长度接近或超过 2048 个字符时,才应该用 POST 获取数据,这是因为「IE 限制 URL 长度」,过长时将会导致请求的 URL 截断

另外需要注意的是:因为响应消息作为脚本标签的源码,所以返回的数据必须是可执行的 javascript 代码,所以你不能使用纯 xml,纯 json 或其它任何格式的数据,无论哪种格式,都必须封装在一个回调函数中

使用 XHR 发送数据到服务器时,GET 方式会更快,因为对于少量数据而言,一个 GET 请求往服务器只发送一个数据包,而一个 POST 请求至少发送两个数据包,一个装载头信息,另一个装载 POST 正文,POST 更适合发送大量数据到服务器

「2.Dynamic script tag insertion(动态脚本标签插入)」:该技术克服了 XHR 的最大限制:它可以从不同域的服务器上获取数据。这是一种黑客技术,而不是实例化一个专用对象,你用 JavaScript 创建了一个新脚本标签,并将它的源属性设置为一个指向不同域的 URL

var scriptElement = document.createElement('script');
scriptElement.src = 'http://any-domain.com/javascript/lib.js';
document.getElementsByTagName_r('head')[0].appendChild(scriptElement);

但是动态脚本标签插入与 XHR 相比只提供更少的控制。你不能通过请求发送信息头。参数只能通过 GET 方法传递,不能用 POST。你不能设置请求的超时或重试,实际上,你不需要知道它是否失败了。你必须等待所有数据返回之后才可以访问它们。你不能访问响应信息头或者像访问字符串那样访问整个响应报文。

最后一点非常重要。因为响应报文被用作脚本标签的源码,它必须是可执行的 JavaScript。你不能使用裸 XML,或者裸 JSON,任何数据,无论什么格式,必须在一个回调函数之中被组装起来。

 var scriptElement = document.createElement('script');
   scriptElement.src = 'http://any-domain.com/javascript/lib.js';
   document.getElementsByTagName_r('head')[0].appendChild(scriptElement);
   function jsonCallback(jsonString) {
      var data = ('(' + jsonString + ')');
   }

在这个例子中,lib.js 文件将调用 jsonCallback 函数组装数据:

jsonCallback({ "status": 1, "colors": [ "#fff""#000""#ff0000" ] });

尽管有这些限制,此技术仍然非常迅速。其响应结果是运行 JavaScript,而不是作为字符串必须被进一步处理。正因为如此,它可能是客户端上获取并解析数据最快的方法。我们比较了动态脚本标签插入和 XHR 的性能,在本章后面 JSON 一节中。

请小心使用这种技术从你不能直接控制的服务器上请求数据。JavaScript 没有权限或访问控制的概念,所以你的页面上任何使用动态脚本标签插入的代码都可以完全控制整个页面。包括修改任何内容、将用户重定向到另一个站点,或跟踪他们在页面上的操作并将数据发送给第三方。使用外部来源的代码时务必非常小心。

「3.Comet 一种 hack 技术」:以即时通信为代表的 web 应用程序对数据的 Low Latency 要求,传统的基于轮询的方式已经无法满足,而且也会带来不好的用户体验。于是一种基于 http 长连接的“服务器推”技术便被 hack 出来。这种技术被命名为 Comet,这个术语由 Dojo Toolkit 的项目主管 Alex Russell 在博文 Comet: Low Latency Data for the Browser 首次提出,并沿用下来。

其实,服务器推很早就存在了,在经典的 client/server 模型中有广泛使用,只是浏览器太懒了,并没有对这种技术提供很好的支持。但是 Ajax 的出现使这种技术在浏览器上实现成为可能, google 的 gmail 和 gtalk 的整合首先使用了这种技术。随着一些关键问题的解决(比如 IE 的加载显示问题),很快这种技术得到了认可,目前已经有很多成熟的开源 Comet 框架。

以下是典型的 Ajax 和 Comet 数据传输方式的对比,区别简单明了。典型的 Ajax 通信方式也是 http 协议的经典使用方式,要想取得数据,必须首先发送请求。在 Low Latency 要求比较高的 web 应用中,只能增加服务器请求的频率。Comet 则不同,客户端与服务器端保持一个长连接,只有客户端需要的数据更新时,服务器才主动将数据推送给客户端。

Comet 的实现主要有两种方式:

【1】基于 Ajax 的长轮询(long-polling)方式

浏览器发出

【2】基于 Iframe 及 htmlfile 的流(http streaming)方式

Iframe 是 html 标记,这个标记的 src 属性会保持对指定服务器的长连接请求,服务器端则可以不停地返回数据,相对于第一种方式,这种方式跟传统的服务器推则更接近。

在第一种方式中,浏览器在收到数据后会直接调用 JS 回调函数,但是这种方式该如何响应数据呢?可以通过在返回数据中嵌入 JS 脚本的方式,如“”,服务器端将返回的数据作为回调函数的参数,浏览器在收到数据后就会执行这段 JS 脚本。

但是这种方式有一个明显的不足之处:IE、Morzilla Firefox 下端的进度栏都会显示加载没有完成,而且 IE 上方的图标会不停的转动,表示加载正在进行。Google 的天才们使用一个称为“htmlfile”的 ActiveX 解决了在 IE 中的加载显示问题,并将这种方法应用到了 gmail+gtalk 产品中。

「5.Multipart XHR」:允许客户端只用一个 HTTP 请求就可以从服务器向客户端传送多个资源,它通过在服务器端将资源打包成一个由双方约定的字符串分割的长字符串并发送到客户端,然后用 javaScript 处理那个长字符串,并根据 mime-type 类型和传入的其它头信息解析出每个资源

multipart XHR 使用了流的功能,通过监听 readyState 为 3 的状态,我们可以在一个较大的响应还没有完全接受之前就把它分段处理,这样我们就可以实时处理响应片段,这也是 MXHR 能大幅提升性能的主要原因

使用 Multipart XHR 的缺点(但是它能显著提升页面的整体性能):

  1. 获得的资源不能被浏览器缓存
  2. 老版本的 IE 不支持 readyState 为 3 的状态和 data:URL(图片不是由 base64 字符串转换成二进制,而是使用 data:URL 的方式创建,并指定 mime-type 为 image/jpeg 使用 readyState 为 3 是因为你不可能等所有数据都传输完成再处理,那样会很慢)

Beacons 技术

使用 javascript 创建一个新的 Image 对象,并把 src 属性设置为服务器上脚本的 URL,该 URL 包含我们要通过 GET 传回的键值对数据(并没有创建 img 元素,也没有插入 DOM),服务器会接收到数据并保存起来,它需向客户端发送任何回馈信息。这种方式是给服务器回传信息最有效的方式,虽然它的优点是性能消耗很小,但它的缺点也显而易见

发送的数据长度限制得相当小

如果要接收服务器端返回的数据一种方式是监听 Image 对象的 load 事件,另外一种方式就是检查服务器返回图片的宽高来判断服务器状态

数据格式

现在 xml 这种数据格式已全然被 json 取代了,原因很多,主要原因是 XML 文件大小太大,解析速度慢,虽然 XPath 在解析 xml 文档时比 getElementsByTagName 快许多,但 XPath 并未得到广泛支持

JSON 相对 xml 来说,文件体积相对更少,通用性强

JSON 数据被当成另一个 JavaScript 文件并作为原生代码执行,为实现这一点,这些数据必须封装在一个回调函数中,这就是所谓的 JSON 填充(JSON with padding)JSON-P

最快的 JSON 格式就是使用数组形式的 JSON-P

使用 JSON-P 必须注意安全性,因为 JSON-P 必须是可执行的 JavaScript,它可能被任何人调用并使用动态脚本注入技术插入到网站,另一方面,JSON 在 eval 前是无效的 JavaScript,使用 XHR 时它只是被当作字符串获取,所以不要把任何敏感数据编码在 JSON-P 中。

理想的数据格式应该是只包含必要的结构,以便你可以分解出每一个独立的字段,所以自定义格式相对来说体积更小点,可以快速下载,且易于解析(只要用 split 函数即可),所以当你创建自定义格式时,最重要的决定之一就是采用哪种分隔符

 var rows = req.responseText.split(/\u0001/);//正则表达式作为分隔符
 var rows = req.responseText.split("\u0001");//字符串作为分隔符(更为保险)

「数据格式总结」

总的来说越轻量级的格式越好,最好是 JSON 和字符串分隔的自定义格式。如果数据集很大或者解析时间问题,那么就使用这两种格式之一;

JSON_P 数据,用动态脚本标签插入法获取。它将数据视为可运行的 JavaScript 而不是字符串,解析速度极快。它也能跨域使用,但不应涉及敏感数据

字符分割的自定义格式,使用 XHR 或动态脚本标签插入的技术提取,使用 split()解析。此技术解析非常大数据集时比 JSON_P 技术略快,而且通常文件尺寸更小。

缓存数据

  • 在服务器,设置 HTTP 头信息以确保你的响应会被浏览器缓存
  • 在客户端,把获取到的信息存储到本地,从而避免再次请求

如果你希望 Ajax 响应能被浏览器缓存,请必须使用 GET 方式发出请求。设置 Expires 头信息是确保浏览器缓存 Ajax 响应最简单的方法,而且其缓存内容能跨页面和跨会话

当然也可以手工管理本地缓存,也就是直接把服务器接收到的数据缓存起来

用习惯了 Ajax 类库了,然后却连自己怎么写一个 XMLHttpRequest 都不知道了,事实上很多 Ajax 类库都有这样那样的局限(比如说不允许你直接访问 readystatechange 事件,这也意味着你必须等待完整的响应接收完毕之后才能开始使用它)所以......

编程实践

​教你写出高性能 JavaScript
image-20210219203823319

构建并部署高性能 JavaScript 应用

​教你写出高性能 JavaScript
image-20210219203907197

工具

image-20210219204257724

本文 demo

https://github.com/dingxiaoxue/shareDemo

前端搞起来[1]扫码关注xiaoyuanlianer666

Reference

[1]

点赞、在看、分享是对作者最大的支持❤️