夏だからGatsbyのランタイム全部消す
2021/08/02夏――
日本人にとって,夏の風物詩といえば花火ですよね.
――
――――
――――――
一方,フロントエンドエンジニアにとって夏の風物詩といえばLighthouseの花火です.
LighthouseはWebページのパフォーマンスや品質を計測するツールであり,このツールには「すべての計測項目で100点を取ると花火が上がる」という演出があります.
というわけで,最近低下気味だった当ブログのパフォーマンスを改善し,Lighthouseで花火を上げてみました.
パフォーマンス低下の原因:TBT
当ブログの最近のパフォーマンスは80点台後半をウロウロするというものでした.そもそもLighthouseのパフォーマンスは,FCPやSIといったメトリックの組み合わせによって以下のように計算されます.
これらの中でも,当ブログで足を引っ張っていたのがTBT(Total Blocking Time)でした.TBTはユーザー操作のブロッキングに関するメトリックです.例えば,ランタイムのJSで重い処理を実行するようなサイトでは,ユーザー操作へ即座に反応できないとしてスコアが下がります.TBTは,現状のLighthouseではパフォーマンススコアに最も大きな影響を与える重要なメトリックです.詳しい定義は本家の解説を参照してください.
ランタイムの調査
では何がTBTスコアを下げてしまっているのかを知るために,まずは そもそもランタイムでどのようなコードが読み込まれているのか を調査してみました.
ふむふむ.結果を次の3つに分類しました.
- Gatsbyのランタイム(コア機能)
- 例:Fast Page Navigation関連(
@reach/router
,gatsby-link
,各種tsx
ファイルなど) - およびそれらのポリフィル(
polyfills.js
← でかい)
- 例:Fast Page Navigation関連(
- Gatsbyプラグインのランタイム
- 例:画像の遅延読み込み,レスポンシブ画像(
lazysizes.js
など)
- 例:画像の遅延読み込み,レスポンシブ画像(
- ムダなもの
- 例:使用していないソーシャルアイコンのSVGデータ(
_networks-db.js
)
- 例:使用していないソーシャルアイコンのSVGデータ(
3に関しては今すぐ削ればいいですね.2に関しても,最近はブラウザネイティブの機能で実現できるものが多く,正直プラグインは不要そうです.
そして,1に関しても不要であるという結論になりました.このランタイムの内訳を見ると,主にFast Page Navigation(以下FPN)のためのコードが多いようです.つまり,当ブログでFPNを捨てるかどうかが問題になります.
FPNとは,ページ遷移を高速に行うための機能です.リンクをクリックしたときに次のページの描画に必要な情報 のみ (たとえばmarkdownテキスト だけ )をfetchし,Reactを使って画面を動的に書き換えるというものです.この機能は,Gatsbyが登場した当時は特に目玉機能として注目されていた記憶があります.
そもそも当ブログにおいてページ遷移の速さは重要なのでしょうか.過去3ヶ月のGoogle Analyticsのデータを見ると,当ブログの直帰率は約85% であり,
ページ遷移はおおよそ数回しか行われていない ことがわかりました.
つまり,約85%のユーザーにとってFPNは不要な機能であり,それ以外のユーザーにとっても,たった数回のページ遷移がちょこっと早くなる程度の恩恵しか無いわけです.それどころか,この機能のためにルーターやらコンポーネントやらポリフィルやらがドバドバ読み込まれるのは迷惑でしか無いはずです.
ということで, ランタイムは全部要らん という結論になりました.
本来ならこの後TBTに関する詳しい調査もしていく予定だったのですが,そもそもランタイムがゼロになったらTBTもゼロになるので調査はここまでとしました.
実装:Gatsbyのランタイム全部消す
gatsby-plugin-no-javascript-utilsというGatsbyプラグインを使ってランタイムを全部消します.他にgatsby-plugin-no-javascriptというプラグインもあったのですが,こちらは最新のGatsbyに正式には対応していないとのことでした.依存関係を無理やり解決させれば動くらしいのですが,なんとなく気持ち悪かったのでやめました.
基本的にこのプラグインを導入するだけでランタイムが全て消えます.しかしこれだけではブログの一部が壊れるので,追加の調整や実装が必要です.ここから先は細かい話になるので,実装に興味のない方は読み飛ばしていただいて結構です.
gatsby-plugin-dark-mode
の置き換え
gatsby-plugin-dark-modeはテーマの切り替えボタンを実装するためのプラグインです.このプラグインではThemeToggler
というカスタムコンポーネントが与えられ,そこにテーマ切り替えのAPIが渡ってきます.しかし,今回はランタイムにReactがいないので,これらのカスタムコンポーネントやAPIは使えません.
ではどうしようかと,このプラグインの実装を詳しく読んでみると,window
にAPIが公開されていることがわかりました.
function setTheme(newTheme) {
//...
window.__theme = newTheme;
//...
}
window.__setPreferredTheme = function (newTheme) {
setTheme(newTheme);
//...
};
そこで,次のように書くことで静的ビルドにおけるハンドラを無理やり宣言しました.
const ThemeToggleButton = () => (
<>
<button className="theme-toggle-button" />
<script
dangerouslySetInnerHTML={{
__html:
"(" +
(() => {
document
.querySelector(".theme-toggle-button")
.addEventListener("click", () =>
__setPreferredTheme(__theme === "light" ? "dark" : "light")
);
}).toString() +
")()",
}}
/>
</>
);
ヤバいコードを書いている自覚はあります.罰は甘んじて受けます.
gatsby-plugin-google-analytics
の置き換え
GatsbyのようなSPAでGoogle Analyticsを正しく運用するには工夫が必要です.前述したとおり,通常のGatsbyにおいてリンクをクリックしても,Reactによるコンポーネントのレンダリングが発生するだけで実際にページ遷移は発生しません.gatsby-plugin-google-analyticsは,そのナビゲーション関連の処理にハンドラを仕込み,リンクがクリックされたタイミングでトラッキングも行ってくれるプラグインです.
しかし,今回の件で当ブログはもはやSPAではなくなりましたので,こんな大層なプラグインは必要なくなりました.代わりに以下のようなローカルプラグインを作りました.
// gatsby-ssr.js
const React = require("react");
const isProduction = process.env.NODE_ENV === "production";
exports.onPreRenderHTML = ({ getHeadComponents, replaceHeadComponents }) => {
if (!isProduction) return;
const head = getHeadComponents();
head.unshift([
React.createElement("link", {
rel: "preconnect",
href: "https://www.googletagmanager.com",
}),
React.createElement("link", {
rel: "dns-prefetch",
href: "https://www.googletagmanager.com",
}),
]);
head.push([
React.createElement("script", {
async: true,
src: "https://www.googletagmanager.com/gtag/js?id=UA-93342312-1",
}),
React.createElement("script", {
dangerouslySetInnerHTML: {
__html: `
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', 'UA-93342312-1');`,
},
}),
]);
replaceHeadComponents(head);
};
gatsby-plugin-twitter
の置き換え
ツイッターの埋め込みスクリプトを配置してくれるgatsby-plugin-twitterですが,どうやらランタイムでスクリプトタグを生成する仕様らしいため置き換えることにしました.以下のようなローカルプラグインを作りました.
// gatsby-node.js
exports.onCreateNode = ({ node }) => {
if (node.internal.type !== "MarkdownRemark") return;
if (
node.internal.content.match(/^<blockquote class="twitter-tweet"/m) !== null
)
node.internal.content +=
'\n<script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>\n';
};
gatsby-plugin-offline
の後始末
gatsby-plugin-offlineは,Service Worker(以下SW)によってオフラインでもサイトを表示できるようにしてくれるプラグインです.当たり前ですが,今回のランタイム削除でこちらのプラグインも動作しなくなりました.それどころか,以前のSWが残っている状態でランタイム無しのサイトを開こうとすると,死の真っ白画面になってしまいました.今回の事とは関係ないですが,このプラグインは以前から挙動が不安定であったため,いっそのこと消してしまいたいと思います.
SWがあれば問答無用で消すという以下のプラグインを仕込みます.
// gatsby-ssr.js
const React = require("react");
const isProduction = process.env.NODE_ENV === "production";
exports.onPreRenderHTML = ({ getHeadComponents, replaceHeadComponents }) => {
if (!isProduction) return;
const head = getHeadComponents();
head.unshift(
React.createElement("script", {
dangerouslySetInnerHTML: {
__html: `(${(() => {
navigator.serviceWorker.getRegistrations().then((registrations) => {
for (let registration of registrations) {
registration.unregister();
}
});
}).toString()})()`,
},
})
);
replaceHeadComponents(head);
};
最後にパフォーマンスを計測
――
――――
――――――
風流ですね…
まとめ
Gatsbyブログのランタイムを全消しすることでLighthouseの花火を上げました.
私のブログは機能が少ないためGatsbyのコア機能を思い切って削ってみましたが,機能が多い他のサイトでは判断が異なってくると思います.また,コア機能を使用しなくなったからといってGatsbyを使う意味がなくなったかというとそんなことはありません.プラグインシステムやコンテンツ管理,GraphQLと型など,気に入っている部分はまだまだ多いです.
また,今回の件を通して,Gatsbyというフレームワーク自体がパフォーマンスに弱いと誤解しないでください.実際に,Gatsby's blog starterの公式サンプルは良い点数を取ります.ただ,何も考えずにプラグインやコンポーネントを追加していくと,私のサイトのようにパフォーマンスが悪くなることもあるのだということです.さらに,今回はパフォーマンスの指標としてLighthouseを用いましたが,これがあらゆる場面において大正義というわけでもありません.あくまで 風流だなぁ という認識です.
何はともあれ,夏に花火を上げられて良かったです.