Nuxt3 (SSG) で Hydration mismatch が起きていた話
2024年12月11日

起きていた問題

本ブログの日付を表示する箇所で、画面の読み込み時 (初回アクセス) に一瞬1日前のものが表示された後に期待通りの日付に戻るという事象が発生していた。また、コンソールからはHydration completed but contains mismatches.というエラーが確認できた。

blog-date-gif

また、ローカルでnpm run generatenpx serve .output/publicで表示させたときはこの事象は起きず、Hydration mismatchs のエラーも出ていなかった。

NuxtのHydrationとは

Nuxtのドキュメントによると、Universal Renderingモード (NuxtにおけるSSRのモード) ではブラウザからのリクエストを受け取るとサーバー側でJavaScriptを実行し静的なHTMLをレンダリングして返すが、ブラウザ側でも裏で同じJavaScriptを実行して動的なDOMをクライアントサイトでも用意する。これによって、単一のHTMLをベースにJavaScriptで内容を書き換えていくSPAのリアクティブ性をSSRにおいても確保している。Nuxt以外でもVue, React (Next.js)など主要なSPAのフレームワークでSSRを採用する場合は同様のHydrationのプロセスが行われていそうである。
npm run generateによるSSGの場合も事前に静的なHTMLを生成してから配信するが、初回アクセス後はページ遷移などでSPAの仮想DOMによるレンダリングがクライアントサイトで行われることは変わらないため、SSGでもHydrationが行われていると考えられる。

本ブログをChromeの開発者ツールで開き、ダウンロードされたリソースを見てみると、確かに初めにダウンロードされる初期ページのHTMLには日付が「2024年07月18日」と書かれている一方、

html

_payload.json?...(ContentfulのAPIに対してuseFetchしたレスポンスが格納されている)では2024-07-18T00:00+09:00となっていて、クライアントサイドでのHydrationが終わったときにHTMLの日付が「2024年07月18日」に上書きされていたようである。

payload_json

原因

day.jsによる日付のフォーマット処理として以下のようなNuxt3のPluginを用意していた。

import dayjs from 'dayjs';

export default defineNuxtPlugin(() => {
  return {
    provide: {
      formatDate(datetime: string): string {
        return dayjs(datetime).format('YYYY年MM月DD日');
      },
    },
  };
});

formatDate()にはContentfulから取得したブログ記事の日付のString"2024-07-18T00:00+09:00"が入ってくるが、クライアントサイドでは日本のタイムゾーンで変換されるので「2024年7月18日」と変換される一方、GitHub ActionsでSSGする際はおそらくデフォルトUTCで実行されるため「2024年7月17日」と変換されてしまっていた。
そこで、以下のようにタイムゾーンを設定し、ビルド時もクライアントサイドの読み込み時も'Asia/Tokyo'で固定することにより、Hydration mismatchのエラーを回避することができた。

import dayjs from 'dayjs';
import utc from 'dayjs/plugin/utc'
import timezone from 'dayjs/plugin/timezone'

dayjs.extend(utc);
dayjs.extend(timezone);

export default defineNuxtPlugin(() => {
  return {
    provide: {
      formatDate(datetime: string): string {
        return dayjs(datetime).tz('Asia/Tokyo').format('YYYY年MM月DD日');
      },
    },
  };
});

(追記)
vueの公式にて、Hydration Mismatchの考えられる主な原因の一つとしてサーバーとクライアントのタイムゾーンが異なることが挙げられていた。今回は日本のタイムゾーンに統一したが、クライアントのタイムゾーンに合わせて表示を変えたい場合は、client-onlyとしてサーバー側ではレンダリングしないことが推奨されている。

https://vuejs.org/guide/scaling-up/ssr.html#hydration-mismatch