在过去的几个月中V8 团队一直努力让新增的 ES2015 和其它更前沿的 JavaScript 功能的性能达到等效的 ES5 的水平
动机
在我们详细介绍各种改进之前我们,首先应该考虑为什么 ES2015+ 功能的性能很重要,尽管 Babel 在现代 Web 开发中得到广泛的应用:
首先,有的 ES2015 功能是按需解析成 ES5 的,例如内置的 Object.assign 。 当 Babel 编译 对象扩展语法 (应用在大量 React 和 Redux 程序)并且编译器也支持这个语法时,Babel 会使用 Object.assign 而弃用等效的 ES5 代码。
将 ES2015 功能解析成 ES5 通常会增加大量代码,加剧了当前的 Web 性能危机 ,尤其不利于新兴市场上常见的千元机。因此,即使在考虑实际执行成本之前,传输、解析和编译代码的成本就相当高。
最后,客户端JavaScript只是依赖于V8引擎的环境之一。 还有用于服务器端应用程序和工具的 Node.js ,开发人员不需要将代码解析成 ES5,可以直接使用目标 Node.js 版本中 相关 V8 版本 支持的功能。
让我们考虑以下节选自 Redux 文档 中的代码段:
function todoApp(state = initialState, action) {
switch (action.type) {
case SET_VISIBILITY_FILTER:
return { ...state, visibilityFilter: action.filter }
default:
return state
}
}
该代码中有两处需要解析成 ES5: 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 生成的 polyfilled_extends 要慢好几个数量级。在这种情况下,从不支持 Object.assign 的浏览器升级到支持 ES2015 的浏览器版本将大幅降低性能,可能会阻碍 ES2015 的普及。
此示例还体现了解析成 ES5 的另一个重要缺点:发送给用户的代码通常远大于开发人员最初编写的 ES2015+ 代码。在上面的示例中,原始代码是 203 字符(gzip 压缩后 176 字节),而生成的代码是 588 字符(gzip 压缩后 367 字节)。体积增长了两倍。 我们来看看 Async Iterators for 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 字符的 ES5 代码(gzip 压缩后 971 字节),这里还没考虑所需依赖的 regenerator runtime :
"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);
};
}();
代码体积增加了 650% ( _asyncGenerator 函数是可复用的,具体取决于捆绑代码的方式,因此可以在多个异步迭代器使用中减小一些代码的体积)。我们不认为将代码解析成 ES5 可以解决所有问题,因为代码体积的增加不仅会影响下载时间/成本,还会增加解析和编译的额外开销。如果我们真的想大幅度地改善现代 Web 应用程序的页面加载和缓存(特别是在移动设备上)的效率,我们必须鼓励开发人员在编写代码时不仅使用 ES2015+,并且不需解析成 ES5 就直接发送给客户端,只向不支持 ES2015 的传统浏览器提供完全解析的代码。对于编译器的作者而言,这一想法意味着我们需要直接支持 ES2015+ 功能, 并 提供合理的性能。