程式碼覆蓋率在效能最佳化上的一種可行應用
2022-08-12由 阿里技術 發表于 農業
測試用例覆蓋率怎麼算
You can‘t manage what you can’t measure。
一件事如果你無法衡量它,你就無法管理它。——管理大師 彼得·德魯克
前 言
JavaScript 是前端應用主要語言,相較於其他平臺程式語言,JS資源多數情況下要透過網路進行載入,那麼程式碼的體積直接影響了頁面載入執行時間。“無效的程式碼”的多寡直接影響到了我們的程式碼質量,所以度量程式碼的執行覆蓋率是一項重要的最佳化前置工作。
什麼是程式碼覆蓋率
1、Dead code
Dead code 也叫無用程式碼,這個概念應是在編譯時靜態分析出的對執行無影響的程式碼,舉個例子:
// 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 是任何情況下都不會執行的程式碼,所以可以再編譯階段將其剔除。
冗餘程式碼 是某些特定的業務邏輯之下並不會執行到這些程式碼邏輯(比如:在首屏載入時,某個前端元件完全不會載入,那麼對於“首屏”這個業務邏輯用例來講,該前端程式碼就是冗餘的)
3、程式碼覆蓋率
程式碼覆蓋率(Code coverage)是軟體測試中的一種度量指標。即描述測試過程中(執行時)被執行的原始碼佔全部原始碼的比例。
怎麼度量程式碼覆蓋率
1、Chrome 瀏覽器 Dev Tools
chrome 瀏覽器的 DevTools 給我們提供了度量頁面程式碼(JS、CSS)覆蓋率的工具 Coverage。
使用方式:Dev tools —— More tools —— Coverage
可度量程式碼型別:JS CSS
統計視覺化形式:
使用率是以byte位元組來計算的;
當我們選擇一段指令碼資源即可在 Source 欄可以看到載入頁面時當前資源 run過得程式碼(藍色)和沒有run過得程式碼(紅色);
缺點:顯然,目前大部分網頁上的JS指令碼基本都是經過混淆壓縮打包過後的產物,對於開發者而言,這種覆蓋率可讀性及參考價值不大。
TIPS:當然,如果在擁有 source map 的情況下也是可以用瀏覽器檢視原始碼的覆蓋率的:
1。
在 source tab 中找到當前頁面的 js 資原始檔(當然已經被混淆的面目全非)
2。
輸入 sourcemap URL(以 def 釋出平臺為例,在構建結果中可找到)
3。 在
webpack://
目錄下即可檢視對應原始碼的大致覆蓋率(不過沒有什麼消費價值)
那麼問題來了,有沒有一種方法可以令開發者瞭解
原始碼
的程式碼覆蓋率的值呢?
2、Istanbul(NYC)
這個軟體以土耳其最大城市伊斯坦布林命名,因為土耳其地毯世界聞名,而地毯則是用來覆蓋的。
Istanbul
或者 NYC(New York City,基於 istanbul 實現) 是度量 JavaScript 程式的程式碼覆蓋率工具,目前絕大多數的node程式碼測試框架使用該工具來獲得測試報告,其有四個測量維度:
line coverage(行覆蓋率-每一行是否都執行了) 【一般我們關注這個資訊】
function coverage(函式覆蓋率-每個函式是否都呼叫了)
branch coverage(分支覆蓋率-是否每個 if 程式碼塊都執行了)
statement
coverage(語句覆蓋率-是否每個語句都執行了)
可以度量的程式碼型別:JS TS
統計視覺化的形式:
HTML
terminal
缺點:目前使用 istanbul 度量網頁前端JS程式碼覆蓋率沒有非侵入的方案,採用的是在編譯構建時修改構建結果的方式埋入統計程式碼,再在執行時進行統計展示。
我們可以使用 babel-plugin-istanbul 外掛在對原始碼在 AST 級別進行包裝重寫,這種編譯方式也叫 程式碼插樁 / 插樁構建(instrument)
3、插樁構建
我們如果要度量這一段程式碼哪些程式碼執行了 哪些程式碼沒有執行,我們會怎麼做呢?
// add。js
function
add
(
a, b
)
{
return
a + b
}
module
。exports = { add }
我們可以很容易的想到加一些“裝飾性”的程式碼在我們的原始碼裡面,那麼當代碼一行一行的執行到某處時,那麼我們就在全域性環境變數中記錄一下:
// 全域性物件記錄了 __coverage__ 記錄了上面程式碼中的語句和函式的執行次數
const
c = (
window
。__coverage__ = {
// “f” 表示每一個 function 被執行的次數
// 當前程式碼只有一個 function 因此,f 陣列只有一個 且記錄值為 0
f: [
0
],
// “s” 表示每一個 statement 被執行的次數
// 3 個 statement 全部都以 0 賦值
s: [
0
,
0
,
0
],
})
// 函式定義是一個語句(statement),那麼我們 +1
c。s[
0
]++
function
add
(
a, b
)
{
// 如果 add 函式(function)被呼叫,f +1,且改呼叫語句 s +1
c。f[
0
]++
c。s[
1
]++
return
a + b
}
// add 被調出語句 s +1
c。s[
2
]++
module
。exports = { add }
istabul 確實也是這麼做的,babel-plugin-istanbul 在構建過程中分析 AST 並將相應統計單元(語句、函式、分支等)做裝飾程式碼的新增,最終在程式碼執行之後,輸出一份 json 格式的資料:
{
“/Users/bairuobing/test/istanbul。js”
:{
“path”
:
“/Users/bairuobing/test/istanbul。js”
,
“s”
:{
“1”
:
1
,
“2”
:
0
,
“3”
:
1
},
“b”
:{
},
“f”
:{
“1”
:
0
},
“fnMap”
:{
// function 的開始結束位置資訊
“1”
:{
“name”
:
“add”
,
“line”
:
1
,
“loc”
:{
“start”
:{
“line”
:
1
,
“column”
:
0
},
“end”
:{
“line”
:
1
,
“column”
:
19
}
}
}
},
“statementMap”
:{
// statement 的開始結束位置資訊
“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 的開始結束位置資訊
}
}
}
當我們在執行程式碼過後,得到了上面的 json 便可以消費它了。
# terminal 形式輸出
nyc report ——reporter
=
text
# HTML 形式輸出
nyc
report ——reporter=lcov ——exclude-after-remap=false
terminal
HTML
程式碼覆蓋率在 iHome Rax開發套件 Tbox 中的應用
tips:tbox 每平每屋 消費者端 本地開發套件
既然我們知道了原始碼的程式碼覆蓋率,我們可以用它為效能最佳化做些什麼貢獻呢?
當工程主 bundle 較大,那麼採用拆包較大的/無用的前端元件來瘦身首屏主 JS 包不失為一種可行的選擇,此時就可以根據程式碼覆蓋率來決定最佳化哪些程式碼。
1、程式碼分割
React。lazy
已經為我們提供了一種不錯的思路,就是利用動態載入模組規範
import()
(webpack對
import()
解析為程式碼分割)的能力來實現前端元件程式碼懶載入/動態載入。
以此為靈感,那麼為何不將某些元件透過動態引入的方式載入,來以此換取首頁 bundle 的瘦身呢?
// 動態引入元件
// ThisIsBigMod
import
{ createElement, useState, useEffect }
from
‘rax’
;
export
default
(props) => {
const
[AsyncMod, setAsyncMod] = useState(
null
);
useEffect(
()
=>
{
const
load =
async
() => {
const
Module =
await
import
(
‘。/ThisIsBigMod’
);
// 關鍵
try
{
setAsyncMod(Module);
}
catch
(e) {
console
。log(e);
}
};
load();
}, []);
if
(!AsyncMod || !AsyncMod。default) {
return
null
;
}
return
<
AsyncMod。default
{
。。。props
} />
;
};
2、下一步
我們能透過程式碼覆蓋率統計出哪些元件的程式碼首屏使用率為0(或者門檻值30%以下),並在專案工程中自動生成一個持久化的檔案配置(app。json中),之後依據配置將這些低使用率的元件程式碼在生產構建時將產物程式碼改寫為動態引入。
於是有了以下方案:
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。 插樁構建
依賴 @ali/build-plugin-coverage
透過插樁將原始碼中插入統計程式碼
本地構建之後頁面全域性會注入
__coverage__
變數(可在頁面控制檯輸出該變數檢查插樁是否成功)
4。 分析自動化生成配置
等待完成首屏渲染(或者完成自定義的一系列行為用例),此刻插樁程式碼已經完成了程式碼使用率的統計
開啟 Tlog 小工具 點選
程式碼最佳化
->
生成原始碼最佳化配置
,此刻 Tbox 本地服務已經接收到了發來的
__coverage__
並完成後續的程式碼覆蓋率分析,透過分析使用率低於門檻值的元件檔案,將這些元件的專案相對路徑寫入
app。json
的 modsPath 欄位下
此刻 @ali/build-plugin-async-components 會根據 modsPath 配置自動將元件構建為動態引入的方式
如果您想透過自己的配置來完成元件非同步化,請直接手動修改 app。json 裡的 modsPath 欄位,只需依賴 @ali/build-plugin-async-components 外掛再次構件即可
此時我們條件載入被非同步化的元件會發現,BigMod 元件已經被動態的拆包引入了,頁面主 js 包也得到了瘦身,搞定!
寫在最後
istanbul 在 node 環境下跑測試用例程式碼能度量覆蓋率是由於其對執行時模組載入器的原始碼攔截,但是比較遺憾的是,本文介紹的程式碼插樁分析覆蓋率這會引入一些多餘的樁程式碼,或許採用 puppeteer 無頭瀏覽器提供的Coverage api + sourceMap 逆編譯的思路來進行度量是一種更加完美的方式,期待與諸君一起探索,繼續努力!