Gatsby プロダクションビルドでのちらつきを直す

October 31, 2021

Gatsby でサイトを構築していて、開発サーバーでは起きなかった表示崩れがプロダクションビルドで発生しました。
初期表示時にFOUC のような画面のがたつきや、ダークモードのちらつきがプロダクションビルドのみに起きるというものです。 原因は SSR と Hydration でしたので調べたことをまとめます。

再現

exampleを clone して yarn build && yarn serve

環境

npx gatsby info
System:
OS: Linux 5.4 Debian GNU/Linux 10 (buster) 10 (buster)
CPU: (8) arm64 unknown
Shell: 5.7.1 - /usr/bin/zsh
Binaries:
Node: 16.10.0 - ~/.anyenv/envs/nodenv/versions/16.10.0/bin/node
Yarn: 1.22.11 - ~/.anyenv/envs/nodenv/versions/16.10.0/bin/yarn
npm: 7.24.1 - ~/.anyenv/envs/nodenv/versions/16.10.0/bin/npm
Browsers:
Firefox: 78.15.0esr
npmPackages:
gatsby: ^4.0.2 => 4.0.2
gatsby-plugin-image: ^2.0.0 => 2.0.0
gatsby-plugin-layout: ^3.0.0 => 3.0.0
gatsby-plugin-mdx: ^3.0.0 => 3.0.0
gatsby-plugin-sharp: ^4.0.1 => 4.0.1
gatsby-remark-copy-linked-files: ^5.0.0 => 5.0.0
gatsby-remark-images: ^6.0.0 => 6.0.0
gatsby-source-filesystem: ^4.0.0 => 4.0.0
gatsby-transformer-yaml: ^4.0.0 => 4.0.0

本題

gatsby のビルドプロセスはの違いはドキュメント の通りです。 ざっくり言うと gatsby build では全てのページの HTML をビルド時 SSR(SSG) し、Hydration を通してクライアントサイドに React アプリを引き継ぎますが、 gatsby develop では SSR しませんので全てのレンダリングがクライアントサイドで行われます。 これは SSR 全般に言えることだと思いますが、レンダリングを行うのはサーバーサイド(この場合 Node.js 環境)なのでレンダリング時にクライアントの情報は分かりません。(window オブジェクトや CSS MediaQuery 等。"シュレディンガーのユーザー" )

window や デバイス情報に依存したコードが含まれている場合、SSR された HTML は完全ではないので two-pass レンダーによって完了します。

Gatsby does a two-pass render for HTML. It loops through your pages first rendering only the body and then takes the result body HTML string and passes it as the body prop to your html.js to complete the render. source: #onRenderBody

これがちらつきの原因となり得ます。 開発サーバーでは全てのレンダリングをクライアントサイドで行うので問題は起きなかったという訳です。

今回僕は Adobe のデザインシステムである Spectrum の React Spectrum を使用していて、 Theming とかは全てお任せしていたわけですが、Spectrum のカラーテーマは preferes-color-scheme メディアクエリに従います。 当然 SSR 時は検出できませんので、設定は遅延されます。 結果、ダークモードが設定された OS でリクエストした場合、SSR で生成された明るいテーマの HTM が表示されたあとに、dark テーマで再レンダーされる最悪の表示になっていました。

解決策

React Spectrum はcolor-scheme以外にもロケールによってフォントファミリーだったり、any-pointer でアクセシビリティ対応とかしてくれてるっぽいのでその辺のデフォルト値を設定することでとりあえず解決しました。 (基本的に PC からのアクセスを想定しているので、デフォルト値を PC 用に設定した) カラーモードは諦めました。

<Provider
theme={darkTheme}
colorScheme="dark"
scale="medium"
locale="ja-JP"
breakpoints={{ S: 0, M: 768, L: 1024 }}
/>

他にはこちら の通り DOM にマウントされる前は何も render しない(SSR 時には何もレンダリングしない)というワークアラウンドもあるようですが、 初期表示を高速にできるメリットは失われますね。SEO は詳しく無いので SEO への影響は分かりません。

prefers-color-schemeクエリで OS のカラーモード設定に従うのは良いプラクティスだと思うのですが、SSR とはトレードオフということですかね。 (レスポンシブのちらつき等も)

develop モードでも SSR するようなフラグが追加されている?みたいです。 #28138