代碼覆蓋率在性能優(yōu)化上的一種可行應(yīng)用

前言

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
  1. 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ù)努力!

原文:https://mp.weixin.qq.com/s/iQy-Y7yxla3HItKjsNTwng

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容