作者:Alon Zakai
編譯:胡子大哈
翻譯原文:http://huziketang.com/blog/posts/detail?postId=58d11a9aa6d8a07e449fdd2a
英文原文:High-performance ES2015 and beyond
** 轉(zhuǎn)載請(qǐng)注明出處,保留原文鏈接以及作者信息**
過(guò)去幾個(gè)月 V8 團(tuán)隊(duì)聚焦于提升新增的 ES2015 的一些性能、提升最近一些其他 JavaScript 新特性的性能,使其能夠達(dá)到或超越相應(yīng)的 ES5 的性能。
出發(fā)點(diǎn)
在我們討論這些不同的改進(jìn)之前,要先了解在當(dāng)前的 Web 開發(fā)中,已經(jīng)有了廣為使用的 Babel 作為編譯器,為什么還要考慮 ES2015+ 的性能問(wèn)題:
- 首先,有一些新的 ES2015 特性是只有 polyfill 時(shí)需要的。例如
Object.assign函數(shù)。當(dāng) Babel 轉(zhuǎn)譯 “object spread property” 的時(shí)候(在 React 和 Redux 中經(jīng)常碰到),就會(huì)依賴Object.assign來(lái)替代 ES5 中相應(yīng)的函數(shù)(如果VM環(huán)境支持的話)。 - polyfill ES2015 的新特性往往會(huì)增加代碼的 size,這些 ES2015 特性卻有助于緩解當(dāng)前的 web 性能危機(jī),尤其像在手機(jī)設(shè)備這樣的新興市場(chǎng)上。在這樣一種情況下,代碼的解析和的成本將會(huì)很高。
- 最后,客戶端的 JavaScript 運(yùn)行環(huán)境只是依賴于 V8 引擎的環(huán)境之一,還有服務(wù)端的 Node.js 應(yīng)用和工具等,它們都不需要轉(zhuǎn)譯成 ES5 代碼,而直接使用最新的 V8 版本就可以使用這些新特性了。
一起來(lái)看一下下面這段 Redux 文檔中的代碼:
function todoApp(state = initialState, action) {
switch (action.type) {
case SET_VISIBILITY_FILTER:
return { ...state, visibilityFilter: action.filter }
default:
return state
}
}
有兩個(gè)地方需要轉(zhuǎn)譯:默認(rèn)參數(shù) state 和 state 作為實(shí)例化對(duì)象進(jìn)行返回。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;
}
}
假設(shè) Object.assign 要比用 Babel polyfill 生成的代碼要慢一個(gè)數(shù)量級(jí)。這樣的情況下,要將一個(gè)本不支持 Object.assign 的瀏覽器優(yōu)化到使它具有 ES2015 能力,會(huì)引起很嚴(yán)重的性能問(wèn)題。
這個(gè)例子同時(shí)也指出了轉(zhuǎn)譯的另一個(gè)缺點(diǎn):轉(zhuǎn)譯生成的代碼,要比直接用 ES2015+ 寫的代碼體積更大。在上面的例子中,源代碼有 203 個(gè)字符(gzip 壓縮后有 176 字節(jié)),而轉(zhuǎn)譯生成的代碼有 588 個(gè)字符(gzip 壓縮后有 367 字節(jié))。代碼大小是原來(lái)的兩倍。下面來(lái)看關(guān)于 “JavaScript 異步迭代器”的一個(gè)例子:
async function* readLines(path) {
let file = await fileOpen(path);
try {
while (!file.EOF) {
yield await file.readLine();
}
} finally {
await file.close();
}
}
Babel 轉(zhuǎn)譯這段 187 個(gè)字符(gzip 壓縮后 150 字節(jié)),會(huì)生成一段有 2987 個(gè)字符(gzip 壓縮后 971 字節(jié))的 ES5 代碼,這還不包括再生器運(yùn)行時(shí)需要加載的額外依賴:
"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);
};
}();
這段代碼的大小是原來(lái)的 6.5 倍,也就是說(shuō)增長(zhǎng)了 650% (生成的 _asyncGenerator 函數(shù)也可能被共享,不過(guò)這依賴于你如何打包你的代碼。如果被共享的話,多個(gè)異步迭代器共用會(huì)分?jǐn)偞a大小帶來(lái)的成本)。我們認(rèn)為長(zhǎng)遠(yuǎn)來(lái)看一直通過(guò)轉(zhuǎn)譯的方式來(lái)支持 ES5 是不可行的,代碼 size 的增加不僅僅會(huì)使下載的時(shí)間變長(zhǎng),而且也會(huì)增加解析和編譯的開銷。如果我們想要徹底改善頁(yè)面加載速度,和移動(dòng)互聯(lián)網(wǎng)應(yīng)用的反應(yīng)速度(尤其在手機(jī)設(shè)備上),那么一定要鼓勵(lì)開發(fā)者使用 ES2015+ 來(lái)開發(fā),而不是開發(fā)完以后轉(zhuǎn)譯成 ES5。對(duì)于不支持 ES2015 的舊瀏覽器,只有給它們完全轉(zhuǎn)譯以后的代碼去執(zhí)行了,而對(duì)于 VM 系統(tǒng),上面所說(shuō)的這個(gè)愿景也要求我們不斷地提升 ES2015 的性能。
評(píng)估方法
正如上面所說(shuō)的,ES2015+ 自身的絕對(duì)性能現(xiàn)在已經(jīng)不是關(guān)鍵了。當(dāng)前的關(guān)鍵是首先一定要確保 ES2015+ 的性能要比純 ES5 高,第二更重要的是一定要比用 Babel 轉(zhuǎn)譯以后的版本性能高。目前已經(jīng)有了一個(gè)由 Kevin Decker 開發(fā)的 six-speed 項(xiàng)目,這個(gè)項(xiàng)目多多少少實(shí)現(xiàn)了我們的需求:ES2015 特性 vs 純 ES5 vs 轉(zhuǎn)譯生成代碼三者之間的比較。
因此我們現(xiàn)在把提升相對(duì)性能作為我們做 ES2015+ 性能提升的基礎(chǔ)。首先將會(huì)把注意力聚焦于那些最嚴(yán)重的問(wèn)題上,即上面圖中所列出的,從純 ES5 所對(duì)應(yīng)的 ES2015+ 版本性能下降 2 倍的那些項(xiàng)。之所以這么說(shuō)是因?yàn)橛袀€(gè)前提假設(shè),假設(shè)純 ES5 的版本至少會(huì)和相應(yīng) Babel 生成的版本速度一樣快。
為現(xiàn)代語(yǔ)言而生的現(xiàn)代架構(gòu)
以前版本的 V8 優(yōu)化像 ES2015+ 這樣的語(yǔ)言是比較困難的。比如想要加一個(gè)異常處理(即 try/chtch/finally)到 Crankshaft (V8 以前版本的優(yōu)化編譯器)是不可能的。就是說(shuō)以 V8 的能力去優(yōu)化 ES6 中的 for...of (這里面隱含有 finally 語(yǔ)句)都是有問(wèn)題的。Crankshaft 在增加新的語(yǔ)言特性到編譯器方面有很多局限性和實(shí)現(xiàn)的復(fù)雜性,這就使得 V8 框架的更新優(yōu)化速度很難跟得上 ES 標(biāo)準(zhǔn)化的速度。拖慢了 V8 發(fā)展的節(jié)奏。
幸運(yùn)的是,lgnition 和 TurboFan (V8 的新版解釋器和編譯器)在設(shè)計(jì)之初就考慮支持整個(gè) JavaScript 語(yǔ)言體系。包括先進(jìn)的控制流、異常處理、最近的 for...of 特性和 ES2015 的重構(gòu)等。lgnition 和 TurboFan 的密集組合架構(gòu)使得對(duì)于新特性的整體優(yōu)化和增量式優(yōu)化成為可能。
許多我們已經(jīng)在現(xiàn)代語(yǔ)言特性上所取得的成功只有在 lgnition/TurboFan 上才可能實(shí)現(xiàn)。 lgnition/TurboFan 在優(yōu)化生成器和異步函數(shù)方面的設(shè)計(jì)尤其關(guān)鍵。V8 一直以來(lái)都支持生成器,但是由于 Crankshaft 的限制,對(duì)其優(yōu)化會(huì)極其受限。新的編譯器利用 lgnition 生成字節(jié)碼,這可以使復(fù)雜的生成器控制流轉(zhuǎn)化為簡(jiǎn)單的本地字節(jié)控制流。TurboFan 也可以更容易實(shí)現(xiàn)基于字節(jié)流的優(yōu)化,因?yàn)樗灰郎善骺刂屏鞯奶厥饧?xì)節(jié),只需要知道如何保存和恢復(fù)函數(shù)聲明就可以了。
聯(lián)合聲明
我們短期目標(biāo)是盡快實(shí)現(xiàn)少于 2 倍的性能改善。首先從最差情況的實(shí)驗(yàn)開始,從 Chrome M54 到 Chrome M58 我們成功的把慢于 2 倍的測(cè)試集從 16 個(gè)降到了 8 個(gè)。同時(shí)也顯著地使緩慢程度的中位數(shù)和平均數(shù)得以降低。
從下圖中我們可以清晰地看到變化趨勢(shì),已經(jīng)實(shí)現(xiàn)了平均性能超過(guò)了 ES5 大概 47%,這里列出的是在 M54 上的一些典型數(shù)據(jù)。
另外我們顯著提高了基于迭代的新語(yǔ)言的性能,例如傳遞操作符和 for...of 循環(huán)等。下面是一個(gè)數(shù)組的重構(gòu)情況:
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” 來(lái)了解更多細(xì)節(jié)的信息。下面這里是我們?cè)?2017 年 1 月 12 日發(fā)出的視頻連接。
我們會(huì)繼續(xù)針對(duì) ES2015+ 的特性提升其性能。如果你對(duì)這一問(wèn)題感興趣,請(qǐng)看我們 V8 的“ES2015 and beyond performance plan。
如果大家對(duì)文章感興趣,歡迎關(guān)注我的知乎專欄-前端大哈。定期發(fā)布高質(zhì)量文章。
我最近正在寫一本《React.js 小書》,對(duì) React.js 感興趣的童鞋,歡迎指點(diǎn)。