リクルートテクノロジーズ でフロントエンドエンジニアをしている@SW20_Toshiです。
本記事はRecruit Engineers Advent Calendar 2019 - Adventar 20日目の記事です。
皆様はウェブのパフォーマンスを気にしていますか?
おそらく大抵の方はSQLのチューニングやロジックの改良などをした経験があるのではないでしょうか?
今回は、レガシーサービスにおけるパフォーマンス改善として、Puppeteerを使って不要なCSSを削除する取り組みを行いました。
ツールはOSSとして公開しているので使ってみてください!
サイボウズ株式会社のこちらの記事には大変お世話になりました!
ありがとうございました!
きっかけ
10年近くABテストや機能追加を繰り返してきたWebサービス....
1画面に大量のCSSファイルが読み込まれていて、カバレッジ関しては目も当てられない酷さ
パフォーマンス・開発者観点で、次のような問題があげられます。
パフォーマンス観点
- カバレッジが低いファイルが複数あり、描画速度が遅くなる
- CSSの解決のされ方についてはこの記事がわかりやすいです
- minifyもconcatもできていない
開発者観点
- CSSが3万行以上あるため、影響範囲がわからない。
- 案件に入る前に、リファクタリングが必要なことも...
やったこと
css-optimization
というツールを開発し、結果としてCSSを180KBの削減に成功しました
light houseのパフォーマンススコアも大幅に改善
before
after
作ったツールについて
そのページの使われているCSSのみを抽出するcss-optimizationというCLIを開発しました。
任意のURLと操作をyamlに記述するだけで、使われているCSSのみを取得することができます。
name: demo
# CSSを最適化したいページのURL
url: 'https://hogehoge/fuga/'
# userAgentを指定
userAgent: 'bot'
steps:
# 良しなにDOM操作をして、モーダルとかを表示
- action:
type: hover
selector: '#js-mylist > div > ul > li:nth-child(2) > div > div > ul'
- action:
type: click
selector: '#js-mylist-myHistory'
- action:
type: wait
duration: 500
# 意図した操作が行われているか、スクショをとる
- action:
type: screenshot
name: 'demo'
前提条件(ログインなど)を定義することも可能です
ツールを開発しリリースへ向けて作業していく上で、躓いた点などを説明していきたいと思います。
Puppeteerとは
puppeteer
とは、GUIを操作することなく、プログラムからAPIでChromeを制御できる ライブラリ です。
Puppeteerでカバレッジを取る方法
// カバレッジ収集を開始
await page.coverage.startCSSCoverage();
// ブラウザを操作しカバレッジを収集したいページを表示する
await page.goto('ここにURLが入ります');
…
// カバレッジ収集を終了
const coverage = await page.coverage.stopCSSCoverage();
coverageの中には、各CSSファイルの内容と、何文字目から何文字目から使われているかが返り値として入っています。
これをゴニョゴニョして最適化されたものだけが取り出されると思いきや、楽にはいきませんでした。
イメージとしてはこんな感じ
const coverage = [
{
url: "ファイルのURL",
text: "ファイルの中身",
ranges: [
{ start: "開始位置", end: "終了位置" },
{ start: "開始位置", end: "終了位置" },
…
]
},
…
]
const optimizedCSSMap = cssCoverage.map(entry => {
const { url, ranges, text } = entry
return {
fileName: convertUrl(url),
coverage: ranges
.map(range => {
return code + text.slice(range.start, range.end) + '\n'
})
.join(''),
}
})
ここで上がってきた課題
-
動的に表示される要素が、使われていないと判定されてしまう
これは現状レンダリングしているものを「使用している」と判断しているためです。
JSによって動作するモーダルやポップアップ、メニューバーがある場合は表示させないと「使用している」と判断されません。
→つまりは、stopCSSCoverageまでにpuppeteerでDOM操作をしなければならない -
media query
やfont-face
などの@ルールが使われていないとみなされる -
コメントを残したい
-
最適化されたCSSを、読み込ませて回帰テストを楽にしたい
どうやって解決したか
動的に表示される要素を考慮したカバレッジを出力する
↓のように、stopCSSCoverage
の前にDOMを操作する処理を書いても良いのですが
他のサービスに横展開するときにハードルが高くなってしまったり、各画面に合わせてソースコードを書き換えるのはしんどいです
// カバレッジ収集を開始
await page.coverage.startCSSCoverage();
// ブラウザを操作しカバレッジを収集したいページを表示する
await page.goto('ここにURLが入ります');
…
// ここに、動的に操作する処理を書く
await page.hover('hoge')
await page.click('fuga')
// カバレッジ収集を終了
const coverage = await page.coverage.stopCSSCoverage();
そこで、pupperiumのyamlでpuppeteer
を操作する機能を流用しました。
steps:
# 良しなにDOM操作をして、モーダルとかを表示
- action:
type: click
selector: '#hoge'
- action:
type: wait
duration: 500
- action:
type: screenshot
name: 'demo'
media queryやfont-face、コメントを残すために
PostCSS
には、ASTを簡単に操作するためのAPIが用意されています。
ASTはJavaScript
のオブジェクトで簡単に取り扱うことができるため、AtRule
とComment
ノードの探索と削除を行います。
const isUnneededNode = (node, coverage) => {
// Root, Comment, Declarationは削除しない
if (['root', 'comment', 'decl'].includes(node.type)) {
return false;
}
// AtRuleは削除しない
if (node.type === 'atrule') {
return false;
}
};
// ASTの探索
root.walk(node => {
// 削除対象か?
if (isUnneededNode(node)) {
// 削除対象ならASTから削除する
node.remove();
}
});
ノードの一覧
- Rootノード: ASTの1番上のノード(Rootノードは親ノードがない)
- Ruleノード: 1つのルールセット
- AtRuleノード: 1つの@ルール
- Declarationノード: 1プロパティ宣言
- Comment: 1つのコメント
回帰テストを楽にする
最適化されたCSSを代わりに読み込ませるためには、setRequestInterception
でリクエストに対するインターセプトを有効し、request.continue
で上書きをすることができます。
// リクエストに対するインターセプトを有効にする
page.setRequestInterception(true)
// リクエストを監視する
page.on('request', request => {
if (scrapingUrl === request.url()) {
// overridesが上書きする内容
request.continue(overrides).catch(err => console.error(err))
} else {
request.abort().catch(err => console.error(err))
}
})
CSSを上書きした後に、同じシナリオで画像比較
わかりやすいように、比較結果をHTMLとして吐き出してくれるCLIも作りました。
最後に
使われていないCSSを削除して、concatするだけでここまでパフォーマンスが改善するとは思いませんでした。
機会があれば、普段のパフォーマンスモニタリングやパフォーマンスバジェットなどもお話しできればと思います。