高性能JavaScript

272 阅读6分钟

JavaScript加载

  • 将所有 <script> 标签尽可能放置在页面的底部,紧靠 body 关闭标签 </body> 的上方,保证页面在脚本 运行之前完成解析样式
  • 将脚本成组打包 / 压缩。页面的 <script> 标签越少,页面的加载速度就越快,响应也更加迅速。(单个文件文件也不宜过大,可利用浏览器并行下载能力, 切割成多个文件)
  • 非阻塞方式下载 JavaScript:动态创建 <script> 元素;用 XHR 对象下载JavaScript代码,并注入到页面中
/***
* 推荐的非阻塞模式(用XHR下载JavaScript代码虽然不立即执行,可能存在跨域)
*/
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);
}

数据访问

  • 局部变量比域外变量快,因为它位于作用域链的第一个对象中。变量在作用域链中的位置越深,访问所需的时间就越长。全局变量总是最慢的,因为它们总是位于作用域链的最后一环。
  • 一个对象的属性或方法在原形链中的位置越深,访问它的速度就越慢,查找属性需要往原型链上遍历搜索,所以尽量使用直接量(简单类型)

DOM 编程

  • 最小化 DOM 访问,修改 DOM 元素会造成重绘和重新排版
  • 在ECMAScript处理数据,最后一次性调用dom操作
  • innerHTMLcreateElement / appendChild 性能比较:在老的浏览器 innerHTML 要快得多,在新的浏览器差距不大,甚至 createElement 表现更好
  • 遍历数组比遍历集合(HTML Collection 类数组)快,集合的 length 属性缓存到一个变量中,遍历集合时可先转化成数组副本
  • 遍历 children 比 childNodes 更快
  • 使用速度更快的 API,诸如 querySelectorAll()和 firstElementChild • offsetTop, offsetLeft, offsetWidth, offsetHeight • scrollTop, scrollLeft, scrollWidth, scrollHeight • clientTop, clientLeft, clientWidth, clientHeight • getComputedStyle() (currentStyle in IE)(在 IE 中此函数称为 currentStyle) 获取布局信息这些属性和方法,需返回最新的数据,浏览器不得不刷新 渲染队列并重排版。所以 不要在布局信息改变时查询它
  • 页面中存在大量元素绑定了事件,使用事件托管

算法和流程控制

  • 优化循环工作量的第一步是减少对象成员和数组项查找的次数

  • 倒序循环是编程语言中常用的性能优化方法

  • forEach 比传统循环(for、while、do while)要慢,每个数组项要关联额外的函数 调用是造成速度慢的原因

  • 尽量减少循环中每次迭代的运算量,并减少循环迭代次数

  • 除非你要迭代遍历一个属性未知的对象,否则不要使用 for-in 循环

  • switch 表达式总是比 if-else 更快,但只有当条件体数量很大时才明显更快。两者间的主要性能区别在于:当条件体增加时,if-else 性能负担增加的程度比 switch 更多

  • 将最常见的(频率高的)条件体放在 if-else 首位(更快作出选择)

  • 将 if-else 组织成一系列嵌套的 if-else 替换 else if 再判断,可以减少条件判断。

  • 当条件体的数目为大量离散值时,使用查表法(对象/数组/Map 直接选择)

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

  • 减少工作量就是最好的性能优化技术。代码所做的事情越少,它的运行速度就越快

  • 使用 memoization 技术,通过缓存先前计算结果为后续计算所重复使用,节省计算时间。

  • 算法本身复杂度优化

响应接口

  • 字符串合并:大多数情况下 String.prototype.concat 比简单的 + 和 += 慢

  • JavaScript 和UI界面在同一个主线程内运行,同一时刻只能其中一个可以运行。这意味着当 JavaScript 代码正在运行时,用户界面不能响应,反之亦然。

  • JavaScript 运行时间不应该超过 100 毫秒。过长的运行时间导致 UI 更新出现可察觉的延迟,从而对整体用户体验产生负面影响。这时可用定时器让出时间片,分解长运行脚本成为较短的片断

/**
 * 用定时器让出时间片, 每25ms后再将任务继续加入执行队列
 * 并且做了限时运行代码,在50ms内直接继续下一次的执行,(执行超过50ms后再让出时间片)
 * @param {array} items 需要处理的数组对象
 * @param {function} process 处理函数
 * @param {fucntion} callback 完成回调函数
*/
function timedProcessArray(items, process, callback) {
    var todo = items.concat(); //create a clone of the original
    setTimeout(function() {
        var start = +new Date();
        do {
            process(todo.shift());
        } while (todo.length > 0 && (+new Date() - start < 50));

        if (todo.length > 0) {
            setTimeout(arguments.callee, 25);
        } else {
            callback(items);
        }
    }, 25);
}

编程实践

  • eval,Function构造器,setTimeout 和 setInterval 允许在程序中运行包含代码的字符串,执行过程中会发生二次评估(二次评估是一项昂贵的操作),且可能是不安全的。所以,避免使用 eval 和 Function 构造器避免二次评估;给 setTimeout() 和 setInterval() 传递函数参数而不是字符串参数
  • 创建对象或数组使用直接量最快,占用较少空间
  • 不要做不必要的工作,不要重复做已经完成的工作
  • 延迟加载、条件预加载
  • 使用速度快的部分
  • 尽量使用原生方法

构建与部署

  • 预处理JavaScript文件:
  • 合并 JavaScript 文件,减少 HTTP 请求的数量
  • JavaScript Minification(紧凑): 剔除 js 文件中一切运行无关的内容,包括注释和不必要的空格
  • JavaScript Compression(压缩):携带 Accept-Encoding 的 HTTP 头(gzip 编码)
  • Caching JavaScript Files:使用 http缓存,通过向文件名附加时间戳或版本号解决缓存问题;使用 HTML 5 离线应用程序缓存
  • 使用内容传递网络(CDN)提供 JavaScript 文件,CDN 不仅可以提高性能,它还可以为你管理压缩和缓 存