Node.js 性能調優之代碼篇(一)——使用原生模塊
每次看 cpuprofile,我都情不自禁地作一個黑人問號???的表情。
由於歷史原因,石墨後端的框架選型是 koa@1+(generator|bluebird)+sequelize。這個選型並沒有什麼問題,也很常見,但是到了濫用的地步。就像封面圖表達的那樣,排除掉 sequelize 這個不得不用的模塊,從 cpuprofile 角度講講為什麼我認為應該用 async/await+Promise 替代 co+generator|bluebird。
我的觀點是:使用原生模塊具有更清晰的調用棧。
下面用 4 個例子對比,看下相同邏輯不同代碼生成的 cpuprofile 中調用棧的信息。
async.js
use strict;const fs = require(fs);const profiler = require(v8-profiler);async function A() { return await Promise.resolve(A);}async function B() { return await A();}(async function asyncWrap() { const start = Date.now(); profiler.startProfiling(); while (Date.now() - start < 10000) { await B(); } const profile = profiler.stopProfiling(); profile.export() .pipe(fs.createWriteStream(async.cpuprofile)) .on(finish, () => { profile.delete(); console.error(cpuprofile export success); })})();
截圖如下:
可以看出:asyncWrap 中調用了 B 函數,B 函數調用了 A 函數,A 函數中 resolve 了一個值。asyncWrap 中還調用了 stopProfiling 函數。
co.js
use strict;const fs = require(fs);const co = require(co);const profiler = require(v8-profiler);function *A() { return yield Promise.resolve(A);}function *B() { return yield A();}co(function *coWrap() { const start = Date.now(); profiler.startProfiling(); while (Date.now() - start < 10000) { yield B(); } const profile = profiler.stopProfiling(); profile.export() .pipe(fs.createWriteStream(co.cpuprofile)) .on(finish, () => { profile.delete(); console.error(cpuprofile export success); })}).catch(console.error);
截圖如下:
可以看出:調用棧非常不清晰,太多沒有用的 co 相關的調用棧。如果 n 個 generator 層層嵌套,就會出現 n 倍的 (anonymous)->onFullfiled->next->toPromise->co->Promise->(anonymous) 調用棧。如果你讀過 co 的源碼可能知道,這是 co 將 generator 解包的過程。其實這個可以通過 yield generator -> yield* generator 來解決。
co_better.js
use strict;const fs = require(fs);const co = require(co);const profiler = require(v8-profiler);function *A() { return yield Promise.resolve(A);}function *B() { return yield *A();}co(function *coWrap() { const start = Date.now(); profiler.startProfiling(); while (Date.now() - start < 10000) { yield *B(); } const profile = profiler.stopProfiling(); profile.export() .pipe(fs.createWriteStream(co_better.cpuprofile)) .on(finish, () => { profile.delete(); console.error(cpuprofile export success); })}).catch(console.error);
截圖如下:
可以看出:相比 co.js 調用棧就清晰了很多,不過相比用 async/await 還是多了些 onFulfilled、next。
co_bluebird.js
use strict;const fs = require(fs);const co = require(co);const Promise = require(bluebird);const profiler = require(v8-profiler);function *A() { return yield Promise.resolve(A);}function *B() { return yield *A();}co(function *coBluebirdWrap() { const start = Date.now(); profiler.startProfiling(); while (Date.now() - start < 10000) { yield *B(); } const profile = profiler.stopProfiling(); profile.export() .pipe(fs.createWriteStream(co_bluebird.cpuprofile)) .on(finish, () => { profile.delete(); console.error(cpuprofile export success); })}).catch(console.error);
截圖如下:
可以看出:相比較 co_better.js,調用棧中多了許多 bluebird 模塊的無用信息。而且這只是非常簡單的示例代碼,要是複雜的業務邏輯代碼生成的 cpuprofile,幾乎沒法看了。
結論:使用 async/await+Promise+命名函數,具有更清晰的調用棧,讓分析 cpuprofile 時不再痛苦。
聰明的你會問:
- bluebird 太好用了,完全放棄不下啊?你可以逐步替換嘛,大部分場景多寫點代碼就可以避免使用 bluebird 了,比如 bluebird 的 .all .map .filter 啥的都可以用 Promise.all 去實現,實在實現起來複雜的那就先用著 bluebird 唄。
- 我現在代碼中大量使用了 yield+generator 怎麼辦?
- 將所有 yield generator 替換成 yield* generator。
- 升級到 node@^8,逐步用 async/await 替換,畢竟 async 函數調用後返回的也是一個 promise 嘛,也是 yieldable 的。
- 性能比較呢?node@8 下 async/await 完勝 co。https://medium.com/@markherhold/generators-vs-async-await-performance-806d8375a01a
yield -> yield* 的坑
上面講到可以將 yield generator -> yield* generator,這裡面有一個坑,也是由於濫用 co 導致的。代碼如下:
const co = require(co)function* genFunc () { return Promise.resolve(genFunc)}co(function* () { console.log(yield genFunc()) // => genFunc console.log(yield* genFunc()) // => Promise { genFunc }})
可以看出:一個 generatorFunction 返回一個 promise,當使用 yield 的時候,co 判斷返回了一個 promise 會繼續幫我們調用它的 then 得到真正的字元串。如果使用 yield*,即用了語言原生的特性而不經過 co,直接返回一個 promise。
解決方法(任選其一):
- function* genFunc -> function genFunc,用 yield genFunc()
- return Promise.resolve(genFunc) -> return yield Promise.resolve(genFunc),用 yield* genFunc()
作業
請讀者自行嘗試 async/await+bluebird 的情況。
async_bluebird.js
use strict;const fs = require(fs);const profiler = require(v8-profiler);const Promise = require(bluebird);async function A() { return await Promise.resolve(A);}async function B() { return await A();}(async function asyncBluebirdWrap() { const start = Date.now(); profiler.startProfiling(); while (Date.now() - start < 10000) { await B(); } const profile = profiler.stopProfiling(); profile.export() .pipe(fs.createWriteStream(async_bluebird.cpuprofile)) .on(finish, () => { profile.delete(); console.error(cpuprofile export success); })})();
結論:調用棧比 co_blueblird.js 的還難懂。。黑人問號???
推薦閱讀:
※參加第11屆D2前端技術論壇,你有什麼收穫?
※為什麼 Node.js 不給每一個.js文件以獨立的上下文來避免作用域被污染?
※如何利用mongodb+node.js完成一個搜索的功能?
※Node.js 適合用來做 web 開發嗎?