新聞中心
You can't manage what you can't measure.

一件事如果你無法衡量它,你就無法管理它?!芾泶髱?彼得·德魯克
前 言
JavaScript 是前端應(yīng)用主要語言,相較于其他平臺編程語言,JS資源多數(shù)情況下要通過網(wǎng)絡(luò)進行加載,那么代碼的體積直接影響了頁面加載執(zhí)行時間。“無效的代碼”的多寡直接影響到了我們的代碼質(zhì)量,所以度量代碼的執(zhí)行覆蓋率是一項重要的優(yōu)化前置工作。
什么是代碼覆蓋率
1.Dead code
Dead code 也叫無用代碼,這個概念應(yīng)是在編譯時靜態(tài)分析出的對執(zhí)行無影響的代碼,舉個例子:
// 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 在編譯時移除這些 dead code以減小代碼體積。?
2.冗余代碼
而代碼覆蓋率里所提到的冗余代碼 和 Dead Code 又略有不同,簡單來說Dead code適用于編譯時,而 Code coverage 適用于運行時。
Dead code 是任何情況下都不會執(zhí)行的代碼,所以可以再編譯階段將其剔除。
冗余代碼是某些特定的業(yè)務(wù)邏輯之下并不會執(zhí)行到這些代碼邏輯(比如:在首屏加載時,某個前端組件完全不會加載,那么對于“首屏”這個業(yè)務(wù)邏輯用例來講,該前端代碼就是冗余的)?。
3.代碼覆蓋率
代碼覆蓋率(Code coverage)是軟件測試中的一種度量指標。即描述測試過程中(運行時)被執(zhí)行的源代碼占全部源代碼的比例。
怎么度量代碼覆蓋率
1.Chrome 瀏覽器 Dev Tools
chrome 瀏覽器的 DevTools 給我們提供了度量頁面代碼(JS、CSS)覆蓋率的工具 Coverage。?
- 使用方式:Dev tools —— More tools —— Coverage
- 可度量代碼類型:JS CSS
- 統(tǒng)計可視化形式:
- 使用率是以byte字節(jié)來計算的;
- 當(dāng)我們選擇一段腳本資源即可在 Source 欄可以看到加載頁面時當(dāng)前資源 run過得代碼(藍色)和沒有run過得代碼(紅色);
- 缺點:顯然,目前大部分網(wǎng)頁上的JS腳本基本都是經(jīng)過混淆壓縮打包過后的產(chǎn)物,對于開發(fā)者而言,這種覆蓋率可讀性及參考價值不大。
TIPS:當(dāng)然,如果在擁有 source map 的情況下也是可以用瀏覽器查看源代碼的覆蓋率的:
(1)在 source tab 中找到當(dāng)前頁面的 js 資源文件(當(dāng)然已經(jīng)被混淆的面目全非)。
(2)輸入 sourcemap URL(以 def 發(fā)布平臺為例,在構(gòu)建結(jié)果中可找到)。
(3)在 webpack:// 目錄下即可查看對應(yīng)源碼的大致覆蓋率(不過沒有什么消費價值)。
那么問題來了,有沒有一種方法可以令開發(fā)者了解 源代碼 的代碼覆蓋率的值呢?
2.Istanbul(NYC)?
這個軟件以土耳其最大城市伊斯坦布爾命名,因為土耳其地毯世界聞名,而地毯則是用來覆蓋的。
Istanbul或者 NYC(New York City,基于 istanbul 實現(xiàn)) 是度量 JavaScript 程序的代碼覆蓋率工具,目前絕大多數(shù)的node代碼測試框架使用該工具來獲得測試報告,其有四個測量維度:
line coverage(行覆蓋率-每一行是否都執(zhí)行了) 【一般我們關(guān)注這個信息】
function coverage(函數(shù)覆蓋率-每個函數(shù)是否都調(diào)用了)
branch coverage(分支覆蓋率-是否每個 if 代碼塊都執(zhí)行了)
statement coverage(語句覆蓋率-是否每個語句都執(zhí)行了)
- 可以度量的代碼類型:JS TS
- 統(tǒng)計可視化的形式:
- HTML
- terminal
- 缺點:目前使用 istanbul 度量網(wǎng)頁前端JS代碼覆蓋率沒有非侵入的方案,采用的是在編譯構(gòu)建時修改構(gòu)建結(jié)果的方式埋入統(tǒng)計代碼,再在運行時進行統(tǒng)計展示。
我們可以使用 babel-plugin-istanbul 插件在對源代碼在 AST 級別進行包裝重寫,這種編譯方式也叫 代碼插樁 / 插樁構(gòu)建(instrument)。
3.插樁構(gòu)建
我們?nèi)绻攘窟@一段代碼哪些代碼執(zhí)行了 哪些代碼沒有執(zhí)行,我們會怎么做呢?
// add.js
function add(a, b) {
return a + b
}
module.exports = { add }
我們可以很容易的想到加一些“裝飾性”的代碼在我們的源碼里面,那么當(dāng)代碼一行一行的執(zhí)行到某處時,那么我們就在全局環(huán)境變量中記錄一下:
// 全局對象記錄了 __coverage__ 記錄了上面代碼中的語句和函數(shù)的執(zhí)行次數(shù)
const c = (window.__coverage__ = {
// "f" 表示每一個 function 被執(zhí)行的次數(shù)
// 當(dāng)前代碼只有一個 function 因此,f 數(shù)組只有一個 且記錄值為 0
f: [0],
// "s" 表示每一個 statement 被執(zhí)行的次數(shù)
// 3 個 statement 全部都以 0 賦值
s: [0, 0, 0],
})
// 函數(shù)定義是一個語句(statement),那么我們 +1
c.s[0]++
function add(a, b) {
// 如果 add 函數(shù)(function)被調(diào)用,f +1,且改調(diào)用語句 s +1
c.f[0]++
c.s[1]++
return a + b
}
// add 被調(diào)出語句 s +1
c.s[2]++
module.exports = { add }
?
istabul 確實也是這么做的,babel-plugin-istanbul 在構(gòu)建過程中分析 AST 并將相應(yīng)統(tǒng)計單元(語句、函數(shù)、分支等)做裝飾代碼的添加,最終在代碼運行之后,輸出一份 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)我們在運行代碼過后,得到了上面的 json 便可以消費它了。
# 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ā)套件。
既然我們知道了源代碼的代碼覆蓋率,我們可以用它為性能優(yōu)化做些什么貢獻呢?
當(dāng)工程主 bundle 較大,那么采用拆包較大的/無用的前端組件來瘦身首屏主 JS 包不失為一種可行的選擇,此時就可以根據(jù)代碼覆蓋率來決定優(yōu)化哪些代碼。
1.代碼分割
React.lazy 已經(jīng)為我們提供了一種不錯的思路,就是利用動態(tài)加載模塊規(guī)范 import() (webpack對import()解析為代碼分割)的能力來實現(xiàn)前端組件代碼懶加載/動態(tài)加載。
以此為靈感,那么為何不將某些組件通過動態(tài)引入的方式加載,來以此換取首頁 bundle 的瘦身呢?
// 動態(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;
};
2.下一步
我們能通過代碼覆蓋率統(tǒng)計出哪些組件的代碼首屏使用率為0(或者門檻值30%以下),并在項目工程中自動生成一個持久化的文件配置(app.json中),之后依據(jù)配置將這些低使用率的組件代碼在生產(chǎn)構(gòu)建時將產(chǎn)物代碼改寫為動態(tài)引入。
于是有了以下方案:
3.如何使用
(1)該功能需要項目下安裝以下 build 插件(如 tbox 新建的項目已安裝以下插件可忽略):
- @ali/build-plugin-coverage
- @ali/build-plugin-async-components
tnpm install --save-dev @ali/build-plugin-coverage @ali/build-plugin-async-components
(2)build.json
// build.json
"plugins": [
......
"@ali/build-plugin-coverage",
[
"@ali/build-plugin-async-components",
{
"active": true
}
]
]
運行 Tbox:
(3)插樁構(gòu)建
- 依賴 @ali/build-plugin-coverage。
- 通過插樁將源碼中插入統(tǒng)計代碼。
- 本地構(gòu)建之后頁面全局會注入__coverage__變量(可在頁面控制臺輸出該變量檢查插樁是否成功)。
(4) 分析自動化生成配置
- 等待完成首屏渲染(或者完成自定義的一系列行為用例),此刻插樁代碼已經(jīng)完成了代碼使用率的統(tǒng)計。
- 打開 Tlog 小工具 點擊代碼優(yōu)化->生成源代碼優(yōu)化配置,此刻 Tbox 本地服務(wù)已經(jīng)接收到了發(fā)來的__coverage__并完成后續(xù)的代碼覆蓋率分析,通過分析使用率低于門檻值的組件文件,將這些組件的項目相對路徑寫入 app.json 的 modsPath 字段下。
- 此刻 @ali/build-plugin-async-components 會根據(jù) modsPath 配置自動將組件構(gòu)建為動態(tài)引入的方式。
- 如果您想通過自己的配置來完成組件異步化,請直接手動修改 app.json 里的 modsPath 字段,只需依賴 @ali/build-plugin-async-components 插件再次構(gòu)件即可。
此時我們條件加載被異步化的組件會發(fā)現(xiàn),BigMod 組件已經(jīng)被動態(tài)的拆包引入了,頁面主 js 包也得到了瘦身,搞定!
寫在最后
istanbul 在 node 環(huán)境下跑測試用例代碼能度量覆蓋率是由于其對運行時模塊加載器的源代碼攔截,但是比較遺憾的是,本文介紹的代碼插樁分析覆蓋率這會引入一些多余的樁代碼,或許采用 puppeteer 無頭瀏覽器提供的Coverage api + sourceMap 逆編譯的思路來進行度量是一種更加完美的方式,期待與諸君一起探索,繼續(xù)努力!
標題名稱:代碼覆蓋率在性能優(yōu)化上的一種可行應(yīng)用
瀏覽路徑:http://fisionsoft.com.cn/article/coihsip.html


咨詢
建站咨詢
