资讯专栏INFORMATION COLUMN

ES2015 的高性能及其改进方向

Aceyclee / 1424人阅读

摘要:的新特性往往会增加代码的,这些特性却有助于缓解当前的性能危机,尤其像在手机设备这样的新兴市场上。联合声明我们短期目标是尽快实现少于倍的性能改善。我们会继续针对的特性提升其性能。定期发布高质量文章。

作者:Alon Zakai

编译:胡子大哈

翻译原文:http://huziketang.com/blog/posts/detail?postId=58d11a9aa6d8a07e449fdd2a

英文原文:High-performance ES2015 and beyond

转载请注明出处,保留原文链接以及作者信息

过去几个月 V8 团队聚焦于提升新增的 ES2015 的一些性能、提升最近一些其他 JavaScript 新特性的性能,使其能够达到或超越相应的 ES5 的性能。

出发点

在我们讨论这些不同的改进之前,要先了解在当前的 Web 开发中,已经有了广为使用的 Babel 作为编译器,为什么还要考虑 ES2015+ 的性能问题:

首先,有一些新的 ES2015 特性是只有 polyfill 时需要的。例如 Object.assign 函数。当 Babel 转译 “object spread property” 的时候(在 React 和 Redux 中经常碰到),就会依赖 Object.assign 来替代 ES5 中相应的函数(如果VM环境支持的话)。

polyfill ES2015 的新特性往往会增加代码的 size,这些 ES2015 特性却有助于缓解当前的 web 性能危机,尤其像在手机设备这样的新兴市场上。在这样一种情况下,代码的解析和的成本将会很高。

最后,客户端的 JavaScript 运行环境只是依赖于 V8 引擎的环境之一,还有服务端的 Node.js 应用和工具等,它们都不需要转译成 ES5 代码,而直接使用最新的 V8 版本就可以使用这些新特性了。

一起来看一下下面这段 Redux 文档中的代码:

function todoApp(state = initialState, action) {
  switch (action.type) {
    case SET_VISIBILITY_FILTER:
      return { ...state, visibilityFilter: action.filter }
    default:
      return state
  }
}

有两个地方需要转译:默认参数 statestate 作为实例化对象进行返回。Babel 将生成如下 ES5 代码:

"use strict";

var _extends = Object.assign || function (target) { 
    for (var i = 1; i < arguments.length; i++) { 
        var source = arguments[i]; 
            for (var key in source) { 
                if (Object.prototype.hasOwnProperty.call(source, key)){ 
                    target[key] = source[key]; 
                } 
            } 
    } 
    return target; 
};

function todoApp() {
  var state = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : initialState;
  var action = arguments[1];

  switch (action.type) {
    case SET_VISIBILITY_FILTER:
      return _extends({}, state, { visibilityFilter: action.filter });
    default:
      return state;
  }
}

假设 Object.assign 要比用 Babel polyfill 生成的代码要慢一个数量级。这样的情况下,要将一个本不支持 Object.assign 的浏览器优化到使它具有 ES2015 能力,会引起很严重的性能问题。

这个例子同时也指出了转译的另一个缺点:转译生成的代码,要比直接用 ES2015+ 写的代码体积更大。在上面的例子中,源代码有 203 个字符(gzip 压缩后有 176 字节),而转译生成的代码有 588 个字符(gzip 压缩后有 367 字节)。代码大小是原来的两倍。下面来看关于 “JavaScript 异步迭代器”的一个例子:

async function* readLines(path) {
  let file = await fileOpen(path);

  try {
    while (!file.EOF) {
      yield await file.readLine();
    }
  } finally {
    await file.close();
  }
}

Babel 转译这段 187 个字符(gzip 压缩后 150 字节),会生成一段有 2987 个字符(gzip 压缩后 971 字节)的 ES5 代码,这还不包括再生器运行时需要加载的额外依赖:

"use strict";

var _asyncGenerator = function () { function AwaitValue(value) { this.value = value; } function AsyncGenerator(gen) { var front, back; function send(key, arg) { return new Promise(function (resolve, reject) { var request = { key: key, arg: arg, resolve: resolve, reject: reject, next: null }; if (back) { back = back.next = request; } else { front = back = request; resume(key, arg); } }); } function resume(key, arg) { try { var result = gen[key](arg); var value = result.value; if (value instanceof AwaitValue) { Promise.resolve(value.value).then(function (arg) { resume("next", arg); }, function (arg) { resume("throw", arg); }); } else { settle(result.done ? "return" : "normal", result.value); } } catch (err) { settle("throw", err); } } function settle(type, value) { switch (type) { case "return": front.resolve({ value: value, done: true }); break; case "throw": front.reject(value); break; default: front.resolve({ value: value, done: false }); break; } front = front.next; if (front) { resume(front.key, front.arg); } else { back = null; } } this._invoke = send; if (typeof gen.return !== "function") { this.return = undefined; } } if (typeof Symbol === "function" && Symbol.asyncIterator) { AsyncGenerator.prototype[Symbol.asyncIterator] = function () { return this; }; } AsyncGenerator.prototype.next = function (arg) { return this._invoke("next", arg); }; AsyncGenerator.prototype.throw = function (arg) { return this._invoke("throw", arg); }; AsyncGenerator.prototype.return = function (arg) { return this._invoke("return", arg); }; return { wrap: function wrap(fn) { return function () { return new AsyncGenerator(fn.apply(this, arguments)); }; }, await: function await(value) { return new AwaitValue(value); } }; }();

var readLines = function () {
  var _ref = _asyncGenerator.wrap(regeneratorRuntime.mark(function _callee(path) {
    var file;
    return regeneratorRuntime.wrap(function _callee$(_context) {
      while (1) {
        switch (_context.prev = _context.next) {
          case 0:
            _context.next = 2;
            return _asyncGenerator.await(fileOpen(path));

          case 2:
            file = _context.sent;
            _context.prev = 3;

          case 4:
            if (file.EOF) {
              _context.next = 11;
              break;
            }

            _context.next = 7;
            return _asyncGenerator.await(file.readLine());

          case 7:
            _context.next = 9;
            return _context.sent;

          case 9:
            _context.next = 4;
            break;

          case 11:
            _context.prev = 11;
            _context.next = 14;
            return _asyncGenerator.await(file.close());

          case 14:
            return _context.finish(11);

          case 15:
          case "end":
            return _context.stop();
        }
      }
    }, _callee, this, [[3,, 11, 15]]);
  }));

  return function readLines(_x) {
    return _ref.apply(this, arguments);
  };
}();

这段代码的大小是原来的 6.5 倍,也就是说增长了 650% (生成的 _asyncGenerator 函数也可能被共享,不过这依赖于你如何打包你的代码。如果被共享的话,多个异步迭代器共用会分摊代码大小带来的成本)。我们认为长远来看一直通过转译的方式来支持 ES5 是不可行的,代码 size 的增加不仅仅会使下载的时间变长,而且也会增加解析和编译的开销。如果我们想要彻底改善页面加载速度,和移动互联网应用的反应速度(尤其在手机设备上),那么一定要鼓励开发者使用 ES2015+ 来开发,而不是开发完以后转译成 ES5。对于不支持 ES2015 的旧浏览器,只有给它们完全转译以后的代码去执行了,而对于 VM 系统,上面所说的这个愿景也要求我们不断地提升 ES2015 的性能。

评估方法

正如上面所说的,ES2015+ 自身的绝对性能现在已经不是关键了。当前的关键是首先一定要确保 ES2015+ 的性能要比纯 ES5 高,第二更重要的是一定要比用 Babel 转译以后的版本性能高。目前已经有了一个由 Kevin Decker 开发的 six-speed 项目,这个项目多多少少实现了我们的需求:ES2015 特性 vs 纯 ES5 vs 转译生成代码三者之间的比较。

因此我们现在把提升相对性能作为我们做 ES2015+ 性能提升的基础。首先将会把注意力聚焦于那些最严重的问题上,即上面图中所列出的,从纯 ES5 所对应的 ES2015+ 版本性能下降 2 倍的那些项。之所以这么说是因为有个前提假设,假设纯 ES5 的版本至少会和相应 Babel 生成的版本速度一样快。

为现代语言而生的现代架构

以前版本的 V8 优化像 ES2015+ 这样的语言是比较困难的。比如想要加一个异常处理(即 try/chtch/finally)到 Crankshaft (V8 以前版本的优化编译器)是不可能的。就是说以 V8 的能力去优化 ES6 中的 for...of (这里面隐含有 finally 语句)都是有问题的。Crankshaft 在增加新的语言特性到编译器方面有很多局限性和实现的复杂性,这就使得 V8 框架的更新优化速度很难跟得上 ES 标准化的速度。拖慢了 V8 发展的节奏。

幸运的是,lgnition 和 TurboFan (V8 的新版解释器和编译器)在设计之初就考虑支持整个 JavaScript 语言体系。包括先进的控制流、异常处理、最近的 for...of 特性和 ES2015 的重构等。lgnition 和 TurboFan 的密集组合架构使得对于新特性的整体优化和增量式优化成为可能。

许多我们已经在现代语言特性上所取得的成功只有在 lgnition/TurboFan 上才可能实现。 lgnition/TurboFan 在优化生成器和异步函数方面的设计尤其关键。V8 一直以来都支持生成器,但是由于 Crankshaft 的限制,对其优化会极其受限。新的编译器利用 lgnition 生成字节码,这可以使复杂的生成器控制流转化为简单的本地字节控制流。TurboFan 也可以更容易实现基于字节流的优化,因为它不要知道生成器控制流的特殊细节,只需要知道如何保存和恢复函数声明就可以了。

联合声明

我们短期目标是尽快实现少于 2 倍的性能改善。首先从最差情况的实验开始,从 Chrome M54 到 Chrome M58 我们成功的把慢于 2 倍的测试集从 16 个降到了 8 个。同时也显著地使缓慢程度的中位数和平均数得以降低。

从下图中我们可以清晰地看到变化趋势,已经实现了平均性能超过了 ES5 大概 47%,这里列出的是在 M54 上的一些典型数据。

另外我们显著提高了基于迭代的新语言的性能,例如传递操作符和 for...of 循环等。下面是一个数组的重构情况:

function fn() {
  var [c] = data;
  return c;
}

比纯 ES5 版本还要快。ES5:

function fn() {
  var c = data[0];
  return c;
}

比 Babel 生成的代码要快的更多。Babel:

"use strict";

var _slicedToArray = function () { function sliceIterator(arr, i) { var _arr = []; var _n = true; var _d = false; var _e = undefined; try { for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) { _arr.push(_s.value); if (i && _arr.length === i) break; } } catch (err) { _d = true; _e = err; } finally { try { if (!_n && _i["return"]) _i["return"](); } finally { if (_d) throw _e; } } return _arr; } return function (arr, i) { if (Array.isArray(arr)) { return arr; } else if (Symbol.iterator in Object(arr)) { return sliceIterator(arr, i); } else { throw new TypeError("Invalid attempt to destructure non-iterable instance"); } }; }();

function fn() {
  var _data = data,
      _data2 = _slicedToArray(_data, 1),
      c = _data2[0];

  return c;
}

你可以到“高速 ES2015” 来了解更多细节的信息。下面这里是我们在 2017 年 1 月 12 日发出的视频连接。

我们会继续针对 ES2015+ 的特性提升其性能。如果你对这一问题感兴趣,请看我们 V8 的“ES2015 and beyond performance plan。

如果大家对文章感兴趣,欢迎关注我的知乎专栏-前端大哈。定期发布高质量文章。

我最近正在写一本《React.js 小书》,对 React.js 感兴趣的童鞋,欢迎指点。

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/86921.html

相关文章

  • js技术 - 收藏集 - 掘金

    摘要:还记得刚开始学习的时候,内存管理前端掘金作为一门高级语言,并不像低级语言那样拥有对内存的完全掌控。第三方库的行代码内实现一个前端掘金前言本文会教你如何在行代码内,不依赖任何第三方的库,用纯实现一个。 (译) 如何使用 JavaScript 构建响应式引擎 —— Part 1:可观察的对象 - 掘金原文地址:How to build a reactive engine in JavaSc...

    Guakin_Huang 评论0 收藏0
  • js技术 - 收藏集 - 掘金

    摘要:还记得刚开始学习的时候,内存管理前端掘金作为一门高级语言,并不像低级语言那样拥有对内存的完全掌控。第三方库的行代码内实现一个前端掘金前言本文会教你如何在行代码内,不依赖任何第三方的库,用纯实现一个。 (译) 如何使用 JavaScript 构建响应式引擎 —— Part 1:可观察的对象 - 掘金原文地址:How to build a reactive engine in JavaSc...

    zhou_you 评论0 收藏0
  • V8 JavaScript 引擎:性能 ES2015+

    摘要:最后,客户端只是依赖于引擎的环境之一。新的编译器管道利用来实现,并生成可以转换生成器控制流到简单的本地控制流的字节码。可以更容易地优化所得到的字节码,因为它不需要知道关于生成器控制流的任何具体内容,只是如何保存和恢复函数的状态。 本文转载自:众成翻译译者:smartsrh链接:http://www.zcfy.cc/article/2978原文:https://v8project.blo...

    songze 评论0 收藏0
  • JS笔记

    摘要:从最开始的到封装后的都在试图解决异步编程过程中的问题。为了让编程更美好,我们就需要引入来降低异步编程的复杂性。异步编程入门的全称是前端经典面试题从输入到页面加载发生了什么这是一篇开发的科普类文章,涉及到优化等多个方面。 TypeScript 入门教程 从 JavaScript 程序员的角度总结思考,循序渐进的理解 TypeScript。 网络基础知识之 HTTP 协议 详细介绍 HTT...

    rottengeek 评论0 收藏0
  • 「不良视频」如何消灭?她手把手教你走出第一步

    摘要:严肃的开场白故事要从深度学习说起。本文从视频分类的角度,对深度学习在该方向上的算法进行总结。数据集熟悉深度学习的朋友们应该清楚,深度学习是一门数据驱动的技术,因此数据集对于算法的研究起着非常重要的作用。是一个比较成功的传统方法与深度学习算 showImg(https://segmentfault.com/img/bV7hQP?w=900&h=330); 不严肃的开场白 视频社交已经成为...

    Invoker 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<