前言
JavaScript 是前端應(yīng)用主要語(yǔ)言,相較于其他平臺(tái)編程語(yǔ)言,JS資源多數(shù)情況下要通過(guò)網(wǎng)絡(luò)進(jìn)行加載,那么代碼的體積直接影響了頁(yè)面加載執(zhí)行時(shí)間?!盁o(wú)效的代碼”的多寡直接影響到了我們的代碼質(zhì)量,所以度量代碼的執(zhí)行覆蓋率是一項(xiàng)重要的優(yōu)化前置工作。什么是代碼覆蓋率
Dead code
Dead code 也叫無(wú)用代碼,這個(gè)概念應(yīng)是在編譯時(shí)靜態(tài)分析出的對(duì)執(zhí)行無(wú)影響的代碼,舉個(gè)例子:
// a.js
const a = 1;
const b = 2; /* dead code */
export default a;
// index.js
import a from './a.js';
export default function() {
console.log(a);
}
通常我們用 Tree Shaking 在編譯時(shí)移除這些 dead code以減小代碼體積。
冗余代碼
而代碼覆蓋率里所提到的冗余代碼 和 Dead Code 又略有不同,簡(jiǎn)單來(lái)說(shuō)Dead code適用于編譯時(shí),而 Code coverage 適用于運(yùn)行時(shí)。
Dead code 是任何情況下都不會(huì)執(zhí)行的代碼,所以可以再編譯階段將其剔除。
冗余代碼 是某些特定的業(yè)務(wù)邏輯之下并不會(huì)執(zhí)行到這些代碼邏輯(比如:在首屏加載時(shí),某個(gè)前端組件完全不會(huì)加載,那么對(duì)于“首屏”這個(gè)業(yè)務(wù)邏輯用例來(lái)講,該前端代碼就是冗余的)
代碼覆蓋率
代碼覆蓋率(Code coverage)是軟件測(cè)試中的一種度量指標(biāo)。即描述測(cè)試過(guò)程中(運(yùn)行時(shí))被執(zhí)行的源代碼占全部源代碼的比例。
怎么度量代碼覆蓋率
Chrome 瀏覽器 Dev Tools
chrome 瀏覽器的 DevTools 給我們提供了度量頁(yè)面代碼(JS、CSS)覆蓋率的工具 Coverage。
- 使用方式:Dev tools —— More tools —— Coverage


可度量代碼類型:JS CSS
統(tǒng)計(jì)可視化形式:
使用率是以byte字節(jié)來(lái)計(jì)算的;
當(dāng)我們選擇一段腳本資源即可在 Source 欄可以看到加載頁(yè)面時(shí)當(dāng)前資源 run過(guò)得代碼(藍(lán)色)和沒有run過(guò)得代碼(紅色);
缺點(diǎn):顯然,目前大部分網(wǎng)頁(yè)上的JS腳本基本都是經(jīng)過(guò)混淆壓縮打包過(guò)后的產(chǎn)物,對(duì)于開發(fā)者而言,這種覆蓋率可讀性及參考價(jià)值不大。
TIPS:當(dāng)然,如果在擁有 source map 的情況下也是可以用瀏覽器查看源代碼的覆蓋率的:1.在 source tab 中找到當(dāng)前頁(yè)面的 js 資源文件(當(dāng)然已經(jīng)被混淆的面目全非)

2.輸入 sourcemap URL(以 def 發(fā)布平臺(tái)為例,在構(gòu)建結(jié)果中可找到)

3. 在 webpack:// 目錄下即可查看對(duì)應(yīng)源碼的大致覆蓋率(不過(guò)沒有什么消費(fèi)價(jià)值)

那么問(wèn)題來(lái)了,有沒有一種方法可以令開發(fā)者了解 源代碼 的代碼覆蓋率的值呢?
Istanbul(NYC)
這個(gè)軟件以土耳其最大城市伊斯坦布爾命名,因?yàn)橥炼涞靥菏澜缏劽?,而地毯則是用來(lái)覆蓋的。
Istanbul或者 NYC(New York City,基于 istanbul 實(shí)現(xiàn)) 是度量 JavaScript 程序的代碼覆蓋率工具,目前絕大多數(shù)的node代碼測(cè)試框架使用該工具來(lái)獲得測(cè)試報(bào)告,其有四個(gè)測(cè)量維度:
line coverage(行覆蓋率-每一行是否都執(zhí)行了) 【一般我們關(guān)注這個(gè)信息】
可以度量的代碼類型:JS TS
統(tǒng)計(jì)可視化的形式:
HTML
terminal
缺點(diǎn):目前使用 istanbul 度量網(wǎng)頁(yè)前端JS代碼覆蓋率沒有非侵入的方案,采用的是在編譯構(gòu)建時(shí)修改構(gòu)建結(jié)果的方式埋入統(tǒng)計(jì)代碼,再在運(yùn)行時(shí)進(jìn)行統(tǒng)計(jì)展示。
我們可以使用 babel-plugin-istanbul 插件在對(duì)源代碼在 AST 級(jí)別進(jìn)行包裝重寫,這種編譯方式也叫 代碼插樁 / 插樁構(gòu)建(instrument)
插樁構(gòu)建
我們?nèi)绻攘窟@一段代碼哪些代碼執(zhí)行了 哪些代碼沒有執(zhí)行,我們會(huì)怎么做呢?
// add.js
function add(a, b) {
return a + b
}
module.exports = { add }
我們可以很容易的想到加一些“裝飾性”的代碼在我們的源碼里面,那么當(dāng)代碼一行一行的執(zhí)行到某處時(shí),那么我們就在全局環(huán)境變量中記錄一下:
// 全局對(duì)象記錄了 __coverage__ 記錄了上面代碼中的語(yǔ)句和函數(shù)的執(zhí)行次數(shù)
const c = (window.__coverage__ = {
// "f" 表示每一個(gè) function 被執(zhí)行的次數(shù)
// 當(dāng)前代碼只有一個(gè) function 因此,f 數(shù)組只有一個(gè) 且記錄值為 0
f: [0],
// "s" 表示每一個(gè) statement 被執(zhí)行的次數(shù)
// 3 個(gè) statement 全部都以 0 賦值
s: [0, 0, 0],
})
// 函數(shù)定義是一個(gè)語(yǔ)句(statement),那么我們 +1
c.s[0]++
function add(a, b) {
// 如果 add 函數(shù)(function)被調(diào)用,f +1,且改調(diào)用語(yǔ)句 s +1
c.f[0]++
c.s[1]++
return a + b
}
// add 被調(diào)出語(yǔ)句 s +1
c.s[2]++
module.exports = { add }
istabul 確實(shí)也是這么做的,babel-plugin-istanbul 在構(gòu)建過(guò)程中分析 AST 并將相應(yīng)統(tǒng)計(jì)單元(語(yǔ)句、函數(shù)、分支等)做裝飾代碼的添加,最終在代碼運(yùn)行之后,輸出一份 json 格式的數(shù)據(jù):
{
"/Users/bairuobing/test/istanbul.js":{
"path":"/Users/bairuobing/test/istanbul.js",
"s":{
"1":1,
"2":0,
"3":1
},
"b":{
},
"f":{
"1":0
},
"fnMap":{ // function 的開始結(jié)束位置信息
"1":{
"name":"add",
"line":1,
"loc":{
"start":{
"line":1,
"column":0
},
"end":{
"line":1,
"column":19
}
}
}
},
"statementMap":{ // statement 的開始結(jié)束位置信息
"1":{
"start":{
"line":1,
"column":0
},
"end":{
"line":3,
"column":1
}
},
"2":{
"start":{
"line":2,
"column":4
},
"end":{
"line":2,
"column":16
}
},
"3":{
"start":{
"line":4,
"column":0
},
"end":{
"line":4,
"column":24
}
}
},
"branchMap":{ // branch 的開始結(jié)束位置信息
}
}
}
當(dāng)我們?cè)谶\(yùn)行代碼過(guò)后,得到了上面的 json 便可以消費(fèi)它了。
# terminal 形式輸出
nyc report --reporter=text
# HTML 形式輸出
nyc report --reporter=lcov --exclude-after-remap=false
- terminal

- HTML


線程池概述代碼覆蓋率在iHome Rax開發(fā)套件 Tbox 中的應(yīng)用
tips:tbox 每平每屋 消費(fèi)者端 本地開發(fā)套件
既然我們知道了源代碼的代碼覆蓋率,我們可以用它為性能優(yōu)化做些什么貢獻(xiàn)呢?
當(dāng)工程主 bundle 較大,那么采用拆包較大的/無(wú)用的前端組件來(lái)瘦身首屏主 JS 包不失為一種可行的選擇,此時(shí)就可以根據(jù)代碼覆蓋率來(lái)決定優(yōu)化哪些代碼。
代碼分割
React.lazy 已經(jīng)為我們提供了一種不錯(cuò)的思路,就是利用動(dòng)態(tài)加載模塊規(guī)范 import() (webpack對(duì)import()解析為代碼分割)的能力來(lái)實(shí)現(xiàn)前端組件代碼懶加載/動(dòng)態(tài)加載。
以此為靈感,那么為何不將某些組件通過(guò)動(dòng)態(tài)引入的方式加載,來(lái)以此換取首頁(yè) bundle 的瘦身呢?
// 動(dòng)態(tài)引入組件
// ThisIsBigMod
import { createElement, useState, useEffect } from 'rax';
export default (props) => {
const [AsyncMod, setAsyncMod] = useState(null);
useEffect(() => {
const load = async () => {
const Module = await import('./ThisIsBigMod'); // 關(guān)鍵
try {
setAsyncMod(Module);
} catch (e) {
console.log(e);
}
};
load();
}, []);
if (!AsyncMod || !AsyncMod.default) {
return null;
}
return <AsyncMod.default {...props} />;
};
下一步
我們能通過(guò)代碼覆蓋率統(tǒng)計(jì)出哪些組件的代碼首屏使用率為0(或者門檻值30%以下),并在項(xiàng)目工程中自動(dòng)生成一個(gè)持久化的文件配置(app.json中),之后依據(jù)配置將這些低使用率的組件代碼在生產(chǎn)構(gòu)建時(shí)將產(chǎn)物代碼改寫為動(dòng)態(tài)引入。于是有了以下方案:

如何使用
1.該功能需要項(xiàng)目下安裝以下 build 插件(如 tbox 新建的項(xiàng)目已安裝以下插件可忽略):
- @ali/build-plugin-coverage
- @ali/build-plugin-async-components
tnpm install --save-dev @ali/build-plugin-coverage @ali/build-plugin-async-components
- build.json
// build.json
"plugins": [
......
"@ali/build-plugin-coverage",
[
"@ali/build-plugin-async-components",
{
"active": true
}
]
]
運(yùn)行 Tbox:
3. 插樁構(gòu)建
依賴 @ali/build-plugin-coverage
通過(guò)插樁將源碼中插入統(tǒng)計(jì)代碼
本地構(gòu)建之后頁(yè)面全局會(huì)注入coverage變量(可在頁(yè)面控制臺(tái)輸出該變量檢查插樁是否成功)

4. 分析自動(dòng)化生成配置
- 等待完成首屏渲染(或者完成自定義的一系列行為用例),此刻插樁代碼已經(jīng)完成了代碼使用率的統(tǒng)計(jì)

打開 Tlog 小工具 點(diǎn)擊代碼優(yōu)化->生成源代碼優(yōu)化配置,此刻 Tbox 本地服務(wù)已經(jīng)接收到了發(fā)來(lái)的coverage并完成后續(xù)的代碼覆蓋率分析,通過(guò)分析使用率低于門檻值的組件文件,將這些組件的項(xiàng)目相對(duì)路徑寫入 app.json 的 modsPath 字段下
此刻 @ali/build-plugin-async-components 會(huì)根據(jù) modsPath 配置自動(dòng)將組件構(gòu)建為動(dòng)態(tài)引入的方式

- 如果您想通過(guò)自己的配置來(lái)完成組件異步化,請(qǐng)直接手動(dòng)修改 app.json 里的 modsPath 字段,只需依賴 @ali/build-plugin-async-components 插件再次構(gòu)件即可

- 此時(shí)我們條件加載被異步化的組件會(huì)發(fā)現(xiàn),BigMod 組件已經(jīng)被動(dòng)態(tài)的拆包引入了,頁(yè)面主 js 包也得到了瘦身,搞定!
寫在最后
istanbul 在 node 環(huán)境下跑測(cè)試用例代碼能度量覆蓋率是由于其對(duì)運(yùn)行時(shí)模塊加載器的源代碼攔截,但是比較遺憾的是,本文介紹的代碼插樁分析覆蓋率這會(huì)引入一些多余的樁代碼,或許采用 puppeteer 無(wú)頭瀏覽器提供的Coverage api + sourceMap 逆編譯的思路來(lái)進(jìn)行度量是一種更加完美的方式,期待與諸君一起探索,繼續(xù)努力!