作者: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
}
}
有两个地方需要转译:默认参数 state
和 state
作为实例化对象进行返回。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 感兴趣的童鞋,欢迎指点。