【Puppeteer + istanbul】Node.jsでページのカバレッジを取得してみる

6 min read読了の目安(約5400字

カバレッジとは?

本記事におけるカバレッジとはコードカバレッジを指します。
コードカバレッジとはコード網羅率とも呼び、プログラムコードの中で実際に使用されている処理の量を表します。
特に今回の内容ではページを構成するjs css htmlの中に存在する、実際の動作に影響を与えている部分をイメージしてもらえば良いかと思います。

例えば...

具体的によくある、わかりやすい例を挙げるとすると、Bootstrapなどのスタイルモジュールなどを使用している場合が考えられます。
少し前のサイトなどではよく見かけるかと思います。

<head>
  <link 
    rel="stylesheet"
    href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" 
    integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" 
    crossorigin="anonymous"
  >
</head>

このようにスタイルシートを読み込んでいますが、大抵の場合全てのスタイルを使用しているわけではありません。
例えばボタンコンポーネントを使いたくてBootstrapを使用した場合でも、実際にはそれ以外の不要なコンポーネントもダウンロードしてきていることになっています。この状態は、コード全体に対して使用されている箇所が少ないのでカバレッジが低いといえるでしょう。


よりありそうな例としては、共通cssなどサイト全体で使われているスタイルを網羅的に記述したスタイルシートなどがある場合、ページ単位で使っているもの、使っていないものが混在し、もしかすると不要なコードが多分に含まれていたりするケースが想定されます。
コードで言えば以下のようなケースです。

<html lang="ja">
<head>
  <meta charset="utf-8" />
  <link rel="stylesheet" href="common.css">
  <title>super page</title>
</head>
<body>
  <main>
    <div class="used-style-header">ヘッダーだよーん<div>
    <p>Hello World</p>
  </main>
</body>
</html>
/* common.css*/
.used-style-header {
  width: 100%;
}
p {
  color: #FFFFFF;
}
/* 以下は使っておらず不要!! */
.not-used-origin-button {
  padding: 1000000px;
  z-index: 300;
}
.not-used-button-2 {
  padding: 1123px;
  z-index: 9876;
}
.not-used-hyper-super-button {
  padding: 1123px;
  z-index: 9876;
}

単一ページに対してこのようなcssが当てられていて、なおかつ使用されていないセレクタの記述がある場合は、それらは不要とみなされます。

なんで気にする必要があるのか

単純に不要な処理まで含まれていると、不要なコードの読み込みを引き起こしてしまうだけでなく、CSSの場合には、ブラウザがCSSオブジェクトモデルを構築するまでページレンダリングを保留してしまい、レンダリング速度が悪化してしまいます。
これらはページの速度改善に取り掛かる上では避けては通れない問題です。

どうやったら確認できる?

もっとも簡単に確認する方法はDevToolを使うことでしょう。
Chromeの場合、以下のようにして使用することができます。

  1. 開発者用ツールを開く
    chromeの設定 > その他のツール > デベロッパーツール

  2. 右上の設定から Coverage機能を選択
    More tools > Coverage

  3. 実行(リロードすると実行される)

使用している割合、バイト数、ファイルのタイプなど様々な情報が確認できます。また、各ファイルごとにどの行が不要かなどの情報も見ることができます。

※赤いラインは不要な部分

単一のページの場合であればこれで問題ありませんが、複数ページに対してや、自動で取得したい時この方法では少々めんどくさいです。
そこで今回は、クローラーを使用して自動でカバレッジを取得してみます。

今回使うもの

  • puppeteer
    https://pptr.dev/
    ChromeもしくはChromiumを制御できるAPIを提供するNodeライブラリ。普段人々がそうするようにブラウザを操作することができ、スクショなど撮れたりするクローラーくん。
  • istanbul
    カバレッジをいい感じに可視化してくれるやつ
    後述しますがnpmパッケージのほうは非推奨になっているので注意

取得してみる

とりあえずコードを見てみましょう。

import puppeteer from "puppeteer";
const url = "https://yoshimok.me";

const browser = await puppeteer.launch({
    headless: true,
    args: ["--no-sandbox"],
  });
  const page = await browser.newPage();
  
  // カバレッジの取得開始
  await page.coverage.startJSCoverage();
  await page.coverage.startCSSCoverage();
  // 対象ページに移動
  await page.goto(url);
  // カバレッジの取得終了
  const [jsCoverage, cssCoverage] = await Promise.all([page.coverage.stopJSCoverage(),page.coverage.stopCSSCoverage()])

これでカバレッジの取得ができます。かなりシンプルですね。
ちなみに複数ページを跨ぐと過去のページのレポートが消されてしまうらしいので、若干注意が必要そうです。

ここで取得してきた情報は以下のようなものになっています。

[
  {
    "url": "https://yoshimok.me/hoge.js",
    "ranges": [
      { "start": 0, "end": 132 },
      { "start": 155, "end": 251 },
      ~~~~~
      { "start": 1514, "end": 1530 }
    ],
    "text": "!function(e){function r(r){ ....",
  },
  {
   ~~~
  }
]

対象のファイルごとに、使用している文字列の範囲がまとめられているようです。
また、もとのテキストがオブジェクトに含まれています。

さて、情報はとってこれましたが、これだけでは普通の人間には理解できません。
ですので、人間がわかる形で表示してみたいと思います。

カバレッジを可視化する

puppeteerからカバレッジは拾ってこれましたが、まだどこが使っている箇所で使っていない箇所かがわからないので、見やすい形で出力していきましょう。

istanbulを使用するのが楽そうだったのですが、npmのページにアクセスすると非推奨になっていたので代用できそうなnpmパッケージを使っていきます。

https://www.npmjs.com/package/puppeteer-to-istanbul
const p2i = require("puppeteer-to-istanbul");
~~~(前略)~~~
const [jsCoverage, cssCoverage] = await Promise.all([page.coverage.stopJSCoverage(),page.coverage.stopCSSCoverage()])
p2i.write(jsCoverage);

これでレポートを生成する準備が整いました。
.nyc_output/配下にイロイロ出力されますが、この時点ではまだ見にくいです。
レポートはnycを使って生成します。

npm i nyc -g

レポートをhtml形式で生成

nyc report --reporter=html

上記を実行するとcoverage/ディレクトリ下にhtml形式で生成されます。 htmlを開くと見やすいレポートが確認できます。

こんな感じで視覚的に見やすいかたちでレポートを出力することができました。

なるべく手間をかけない形で出力する(未達)

ここまでの処理を、対象のurlを複数入力して出力できるようにします。
イメージとしては以下のような形です。


const urls = [
  "https://hoge.com",
  "https://fuga.com",
  "https://hoge.com/user/i",
] 

const crawl = (url) => {
  // 上で書いたカバレッジ取得処理
}

urls.map(crawl)

こんな感じでしょうか?
出力内容を工夫することで、htmlを流用して対象urlごとにレポートをページングすることでもできそうです。
できたらまた記事を書こうかと思います!

カバレッジにおける注意点

ここまでカバレッジレポートの生成の実装をしてきましたが、注意しなければならない点が、未使用なコードだからといって無闇に削除してよいわけではないという点です。

どういうことかと言うと、カバレッジレポートを使って未使用のコードを見つけることはできますが、実際に使用されているかどうか、本当に不要な記述かどうかはその時点での実装の経緯などまでを考えなければ正しいリファクタリングは行え場合が多いです。(Chromeのディベロッパーは「これらのリファクタリングはテクノロジスタックに大きく依存する」と言っています。)

こららの情報をもとに、議論をしたうえでコードのリファクタリングを行う形が良いと思われます。

今後の課題

今後やりたいことです。

  • 複数URLの実行の処理実装
  • ある程度不要な処理が消されたソースコードの生成
    カバレッジ情報に使用されているコードの範囲が記述されているので、それらをつなぎ合わせればなんとかなりそうな気はしますが、それだときれいに削除できなかったので、構造木的に不要な箇所をコメントアウトしたコードを生成してみたい。

参考など