ふとSRE的なことをやりたくなって、GitHub Pages上の自分のポートフォリオのパフォーマンス改善を行いました。
ポートフォリオサイト
中身がアレなのは今回は無視してください!!!
site: https://reireias.github.io
repo: https://github.com/reireias/reireias.github.io
計測環境の用意
改善するにあたり何らかの指標が必要です。
基本はLightHouseを利用する方向で問題ないですが、今回は実行環境に左右されず、APIがあるPageSpeed Insightsを採用することにしました。
PageSpeed Insightsの結果はLightHouseの結果を内包しており、結果のjson中に含まれるlighthouseResult
オブジェクトで確認できます。
また、上記のjsonをLighthouse Report Viewerに入力することで、LighthouseのレポートをWebUIで確認できます。
これを毎回手動で行うのは大変なので、Node.jsでスクリプト化し、yarn benchmark
で素早く実行できるようにします。
https://github.com/reireias/reireias.github.io/blob/source/scripts/benchmark.js
/* eslint no-console: 0 */
const fs = require('fs')
const qs = require('qs')
const axios = require('axios')
const dateFormat = require('dateformat')
require('dotenv').config()
const LIMIT = 10
const TARGET_URL = process.argv[2]
const VERSION = process.argv[3]
const PAGE_SPEED_INSIGHTS_URL =
'https://www.googleapis.com/pagespeedonline/v5/runPagespeed'
const waitDeploy = async () => {
let count = 0
while (count < LIMIT) {
const res = await axios.get(TARGET_URL)
if (res.data.indexOf(VERSION) > 0) {
return true
}
await new Promise(resolve => setTimeout(resolve, 60 * 1000))
count += 1
}
return false
}
const saveJsonFile = (obj, client) => {
const current = new Date()
const dateString = dateFormat(current, 'yyyymmddhhMMss')
const path = `./tmp/raw/${client}-raw-${dateString}-${VERSION}.json`
fs.writeFileSync(path, JSON.stringify(obj))
const lhPath = `./tmp/${client}-lh-${dateString}-${VERSION}.json`
fs.writeFileSync(lhPath, JSON.stringify(obj.lighthouseResult))
}
const main = async () => {
if (await waitDeploy()) {
;['desktop', 'mobile'].forEach(async client => {
const params = {
url: TARGET_URL,
locale: 'ja',
category: [
'accessibility',
'best-practices',
'performance',
'pwa',
'seo'
],
strategy: client
}
if (process.env.PAGE_SPEED_INSIGHTS_URL) {
params.key = process.env.PAGE_SPEED_INSIGHTS_URL
}
const result = await axios.get(PAGE_SPEED_INSIGHTS_URL, {
params: params,
paramsSerializer: params =>
qs.stringify(params, { arrayFormat: 'repeat' })
})
if (result.status !== 200) {
console.error(result)
throw new Error('Insight failed.')
}
saveJsonFile(result.data, client)
})
} else {
throw new Error('Not deployed yet.')
}
}
;(async () => {
try {
await main()
} catch (err) {
// eslint-disable-next-line no-console
console.error(err)
}
})()
package.json
ではyarn benchmark
を定義します。
"benchmark": "node scripts/benchmark.js https://reireias.github.io $(git rev-parse HEAD)"
GitHub Pagesのデプロイ後の反映には数分かかるので、commitハッシュをmetaタグに埋め込み、最新のページが取得できるようになってからPageSpeed InsightsのAPIを実行するように工夫しています。
実行すると、tmp
ディレクトリ以下にLighthouseの結果がdesktop版とmobile版の2種が出力されます。
(後半はコミットハッシュになっているので、あとからどの時点のコードの結果なのか追えるようにしてあります。)
desktop-lh-20190612093848-38a02b5f9b08ffd3e468f7e70e4861eee74cd8d3.json
mobile-lh-20190612093845-38a02b5f9b08ffd3e468f7e70e4861eee74cd8d3.json
また、tmp/raw
ディレクトリ以下にはそれぞれのPageSpeed Insightsの結果が出力されます。
今回はLighthouseの結果のmobile版を指標としてパフォーマンス改善を行っていきます。
CI/CDの準備
高速に改善サイクルを回すためにはCI/CDが不可欠です。
このリポジトリではTravisCIを使って、テスト(lint)が通ったらGitHub Pagesにデプロイする設定にしています。
---
language: node_js
node_js:
- 'stable'
env:
secure: xxxxxx
cache: yarn
script:
- yarn lint
- yarn generate
- yarn dropcss
deploy:
provider: pages
target_branch: master
skip_cleanup: true
github_token: $GITHUB_TOKEN
local_dir: dist
on:
branch: source
notifications:
email: false
slack:
secure: xxxxxxx
現状把握
では、改善前の結果を見てみましょう。
この時点のソースコードは下記になります。
https://github.com/reireias/reireias.github.io/tree/1b31298ee49fb4788bc6cf150b05c6f5e4a8844d
全体の結果は次のようになっていました。
パフォーマンスの数値が特に低いです。
SEOなどはきちんと実装していたので、100点でした。
この改善できる項目を順に対応していきます。
改善1: 次世代フォーマットでの画像の配信
詳細を見ると、JPEG 2000、JPEG XR、WebPなどの次世代画像フォーマットを利用することが推奨されています。
今回は変換の容易性やブラウザのサポート状況からWebPフォーマットを採用することとします。
対応状況: https://caniuse.com/#feat=webp
Safariがネックですね。(IEなんてこの世に無い、いいね?)
pictureタグを利用することで未対応ブラウザには従来の画像を、対応ブラウザにはWebPを出し分けることができるので、これで対応します。
参考: https://blog.jxck.io/entries/2016-03-26/webp.html
webpへの変換はcwebp
コマンドで行います。
ワンライナーでディレクトリ以下のpngファイルを一気に変換しています。
# one liner
for f in $(find ./ -name "*.png"); do base=$(basename $f | sed -e 's/\.png//g'); cwebp $f -o ${base}.webp; done
pictureタグを使った出し分けの実装は下記のようになります。
<!-- before -->
<img :src="skill.image" alt="" />
<!-- after -->
<picture>
<source type="image/webp" :srcset="`${skill.image}.webp`" />
<img :src="`${skill.image}.png`" :alt="skill.name" />
</picture>
この時点での実装は下記になります。
https://github.com/reireias/reireias.github.io/tree/664262bdb04d9bc88cfc22c0c3366b7b015620b7
むしろパフォーマンスが48点から44点に悪化しました。
改善2: レンダリングを妨げるリソースの除外
画像系はいったん置いておき、別の部分を改善していきます。
下記画像にあるように、外部のウェブフォントとcssが遅れの原因となっています。
ウェブフォントをやめ、以下を参考にクライアントの環境にインストールされているフォントを利用するようにcssを設定する方針にします。
参考: https://sole-color-blog.com/blog/1380/
この方法だとVuetifyのiconが利用できなくなりますが、今回は利用していないので、無視します。
また、TwitterやGitHubのアイコンも今回は諦めます。
どうしてもアイコンで表示したいなら、後述の画像を遅延ロードする方法で対応します。
改善後のソースコードはこちら。
https://github.com/reireias/reireias.github.io/tree/a48ab96b6db533ea4bd9e0494979c811922a2003
パフォーマンスが44->82にまで向上しました!
ウェブフォントの利用(特に日本語)はやはりパフォーマンスとのトレードオフになってしまう傾向にあるようです。
(もちろん、遅延ロードするなりで対策はできるのですが)
改善3: 適切なサイズの画像
WebP化したことで、パフォーマンスに改善は見られなかったので、今度はサイズの方を対応していきます。
画像を正方形の透過pngにする
もとの実装では横長の画像であれば問題なく表示される実装でしたが、全体のサイズ統一のため、これらをいったん正方形にします。
第一引数のファイルを正方形にキャンバス拡大するスクリプト
#!/bin/bash -e
# pngを正方形になるようにキャンバスサイズの拡大を行うスクリプト
# 拡大部分は透過に指定され、もと画像は中央に配置される
file=$1
width=$(identify -format "%w" $file)
height=$(identify -format "%h" $file)
if [[ $width -gt $height ]]; then
size=$width
convert $file -background none -gravity center -extent ${size}x${size} sq.$(basename $file)
fi
if [[ $width -lt $height ]]; then
size=$height
convert $file -background none -gravity center -extent ${size}x${size} sq.$(basename $file)
fi
すべてのpngファイルに対し上記スクリプトを実行し、リネーム
find ./ -name "*.png" | xargs -I{} ~/scripts/resize-square.sh {}
for f in $(find ./ -name "sq.*.png"); do name=$(basename $f | sed -e 's/sq\.//g'); mv $f $name; done
pngを200x200と100x100の2サイズ作成
今回の実装では、表示領域は最大でも100x100程度での表示なので、200x200にすべてリサイズします。(100x100ではWebP変換後に少しぼやけたため)
# mongrifyはconvertコマンドの上書き版
mogrify -resize 200x200 *.png
また、モバイル用に100x100のpngも用意します。
for f in $(find ./ -name "*.png"); do base=$(basename $f | sed -e 's/\.png//g'); convert -resize 100x100 $f ${base}-small.png; done
それぞれをWebP画像に変換
cwebp
コマンドでwebp画像を生成します
for f in $(find ./ -name "*.png"); do base=$(basename $f | sed -e 's/\.png//g'); cwebp $f -o ${base}.webp; done
画面サイズに応じて画像を出し分ける
画面の横幅に応じて利用する画像をpictureタグを用いて切り替えます。
この実装により、大きいWebP画像、小さいWebP画像、png画像がデバイス/ブラウザに応じて表示されるようになります。
<!-- before -->
<picture>
<source type="image/webp" :srcset="`${skill.image}.webp`" />
<img :src="`${skill.image}.png`" :alt="skill.name" />
</picture>
<!-- after -->
<picture>
<source media="(min-width: 600px)" type="image/webp" :srcset="`${skill.image}.webp`" />
<source type="image/webp" :srcset="`${skill.image}-small.webp`" />
<img :src="`${skill.image}.png`" :alt="skill.name" />
</picture>
結果
改善後のソースコードはこちら。
https://github.com/reireias/reireias.github.io/tree/21781e948fff0f517f4d5e9789989e7a437dc46d
パフォーマンスは82->83と微々たる改善でした。
改善4: インタラクティブになるまでの時間の短縮
計測結果を見るに、インタラクティブになるまでの時間がネックっぽいので、次はここを改善します。
pictureタグに対応した画像遅延ロードライブラリLazysizes
を導入することで、操作可能になるまでの時間の短縮を図ります。
参考: https://oxynotes.com/?p=10810
パッケージを追加。
yarn add lazysizes
READMEを確認しながら、下記のようにlazyloadクラスを適用します。
<!-- before -->
<picture>
<source
media="(min-width: 600px)"
type="image/webp"
:srcset="`${skill.image}.webp`"
/>
<source type="image/webp" :srcset="`${skill.image}-small.webp`" />
<img :src="`${skill.image}.png`" :alt="skill.name" />
</picture>
<!-- after -->
<picture>
<source
media="(min-width: 600px)"
type="image/webp"
:data-srcset="`${skill.image}.webp`"
/>
<source
type="image/webp"
:data-srcset="`${skill.image}-small.webp`"
/>
<img
class="lazyload"
:data-src="`${skill.image}.png`"
:alt="skill.name"
/>
</picture>
この実装により、画面外にある画像ファイルはスクロールしていかないとロードされないようになりました。
改善後のソースコードはこちら。
https://github.com/reireias/reireias.github.io/tree/b8dcc4406ab6ce06cc10e01a449d434b33ad8d22
あと少し!!
改善5: html 要素に [lang] 属性が指定されていません
パフォーマンスではなく、ユーザー補助の項目です。
これは単純に実装忘れなので、以下のようにnuxt.config.js
中のhead内にhtmlAttrs
を追加します。
export default {
mode: 'universal',
head: {
title: pkg.name,
htmlAttrs: {
lang: 'ja'
},
...
改善後の結果はこちら。
(しれっとパフォーマンスが99になってますが、計測誤差でしょう)
改善6: 最大推定 FID を少しでも減らす
パフォーマンスが安定して99or100が出るように少しでも改善します。
使用していない CSS を削除してください 15 KB 減らせます
調査したところ、利用しているVue.js用のUIライブラリVuetify.jsにはこれ以上cssを小さくする方法はないようです。
そこで今回は、htmlとcssファイルを入力すると、未使用のcssを削除したcssを出力してくれるdropcssを利用してcssのコード量を削減します。
ちょっとやっかいなのは、Nuxt.jsで静的サイトをジェネレートした際のcssの出力です。
デフォルトではheadタグ内のstyleタグにcssが記述されます。
ただ、この状態ではdropcssを適用することができないため、styleタグをcssに分離する必要があります。
Nuxt.jsのビルド設定にextractCSSオプションを渡すとcssを外部ファイル化し、それをlinkタグで読み込んでくれるようになるのですが、バグなのか一部cssにpreload属性が付かず、パフォーマンスが悪化してしまいました。
なので、今回はstyleタグからinnerHTMLを取り出し、それをcssファイルとして読み込み、dropcssによる圧縮後にまたinnerHTMLに戻すという強引な手法で行います。
上記を実行するスクリプトは以下になります。
/* eslint no-console: 0 */
const fs = require('fs')
const dropcss = require('dropcss')
const cheerio = require('cheerio')
const htmlPath = './dist/index.html'
const html = fs.readFileSync(htmlPath).toString()
const $ = cheerio.load(html, { decodeEntities: false })
$('style').each((_, element) => {
const css = element.children[0].data
const cleaned = dropcss({
html,
css
})
element.children[0].data = cleaned.css
})
fs.writeFileSync(htmlPath, $.html())
改善後のソースコードはこちら。
https://github.com/reireias/reireias.github.io/tree/e1d69f86338b98997d8143727d0fb46e880229cd
何度か計測した結果、パフォーマンスの値が99で安定しました!
改善8: トップの画像にも遅延ロードを導入
Chrome Developer Toolで遅いネットワークをシミュレートしながらサイトを見ていたところ、トップの画像のロードが他をブロックしているように見えたので、ここにも遅延ロードを入れます。
しかし、改善4と同様に遅延ロードを入れてしまうと、ファーストビューに含まれているため、image brokenアイコンが一瞬表示されてしまい、よろしくありません。
そこで、cssで調整します。
lazysizes
の機能でロード中とロード後でclassが変わるので、そこにcssを適用します。
今回はopacityを設定してフェードインするようにします。
.lazyload,
.lazyloading {
opacity: 0;
}
.lazyloaded {
opacity: 1;
transition: opacity 300ms;
}
パフォーマンスは変化しなかったので、結果は割愛します。
まとめ
GiHub PagesにホストしているNuxt.jsで作成した静的ページのパフォーマンスを改善してみました。
改善の方法はどれも簡単なものなので、みなさんも自分のサイトや担当サービスで実施してみてはいかがでしょうか?
おまけ
ちなみに、スコアは高いですがCDNのキャッシュ時間の設定で警告?がまだ残っています。
これはGitHub Pagesを使ってる以上回避不可能なようなので、今回は無視することにしました。
参考: https://qiita.com/Duct-and-rice/items/9f9763d322eb2c98a90f