🙌

SSRにおけるnew Date

に公開

はじめに

Next.jsなどのフレームワークを使ってサーバーサイドレンダリング(SSR)を行うアプリケーションを開発していると、以下のような警告メッセージに遭遇したことはありませんか?

Route "/" used new Date() instead of using performance or without explicitly calling await connection() beforehand. See more info here: https://nextjs.org/docs/messages/next-prerender-current-time

この記事では、SSR環境におけるnew Date()の使用に関する問題点と、その対処法について詳しく解説します。特にNext.jsを使用している開発者にとって、この問題を理解することは重要です。

問題の本質: なぜSSRでnew Date()が問題なのか

1. SSRの仕組みを理解する

サーバーサイドレンダリング(SSR)では、ユーザーがページにアクセスする前に、サーバー側でHTMLを生成します。Next.jsでは、この処理は以下のタイミングで発生します:

ビルド時(next build実行時)
サーバー起動時
ユーザーリクエスト時(ISRやオンデマンドレンダリングの場合)

2. new Date()の問題点

SSR環境でnew Date()(またはdayjs()などのラッパー)を使用すると、以下の問題が発生します:

時刻のフリーズ: ページがプリレンダリングされたタイミングで時刻が固定されてしまう

キャッシュとの不整合: プリレンダリングされたページがCDNやブラウザにキャッシュされると、古い時刻情報がそのまま表示される

ハイドレーション不一致: クライアント側とサーバー側で時刻が異なるため、ハイドレーション(再水和)時に不一致が発生する可能性がある

例えば、以下のようなコードがあるとします:

export function formatRelativeDate(dateString: string): string {
  const date = dayjs(dateString);
  if (date.isValid()) {
    const now = dayjs(); // ここが問題!
    if (now.diff(date, "day") === 0) return "今日";
    return date.fromNow(); // 「1ヶ月前」「1年前」などの相対表示
  }
  return dateString;
}

このコードがSSRで実行されると、dayjs()はビルド時または初回レンダリング時の時刻を返します。これにより、「今日」や「1時間前」などの相対時間表示が正確ではなくなってしまいます。
解決策:SSRでの日付処理を適切に行う方法

  1. クライアントコンポーネントとして実装する
    Next.jsの場合、日付に関する処理をクライアントサイドに移動することが最も簡単な解決策です:
"use client"; // クライアントコンポーネントとして宣言

import dayjs from 'dayjs';
import { useState, useEffect } from 'react';

export function RelativeDate({ dateString }: { dateString: string }) {
  const [formattedDate, setFormattedDate] = useState(dateString);
  
  useEffect(() => {
    const date = dayjs(dateString);
    if (date.isValid()) {
      const now = dayjs();
      if (now.diff(date, "day") === 0) {
        setFormattedDate("今日");
      } else {
        setFormattedDate(date.fromNow());
      }
    }
  }, [dateString]);
  
  return <span>{formattedDate}</span>;
}

このアプローチでは、初回レンダリング時にはdateStringがそのまま表示され、クライアントサイドでハイドレーション後に正確な相対時間が計算されます。

  1. 日付ロジックを分離する
    別のアプローチとして、日付の表示ロジックとフォーマット処理を分離する方法があります:
// サーバーサイドで静的なフォーマットを行う関数
export function formatDate(dateString: string): string {
  const date = dayjs(dateString);
  if (date.isValid()) {
    return date.format('YYYY年MM月DD日');
  }
  return dateString;
}

// クライアントサイドで相対時間を計算するコンポーネント
"use client";
export function RelativeTime({ date }: { date: string }) {
  const [relativeTime, setRelativeTime] = useState('');
  
  useEffect(() => {
    const dateObj = dayjs(date);
    const now = dayjs();
    if (now.diff(dateObj, "day") === 0) {
      setRelativeTime("今日");
    } else {
      setRelativeTime(dateObj.fromNow());
    }
  }, [date]);
  
  return <span>{relativeTime}</span>;
}
  1. Next.jsのconnection()を使用する
    Next.jsのドキュメントで言及されているconnection()を使用する方法もあります。
    これは続きの処理はユーザーからのリクエストがきてから行うように待機することができます。

https://nextjsjp.org/docs/app/api-reference/functions/connection#article

import { connection } from "next/server";

export async function getServerSideProps() {
  // データベース接続を確立(または他の一貫性を保証する処理)
  await connection();
  
  // これ以降のnew Date()は安全に使用可能
  const now = new Date();
  
  return {
    props: {
      serverTime: now.toISOString(),
      // ...他のprops
    }
  };
}
  1. performance.now()を使用する(限定的なケース)
    特定のケースでは、performance.now()を使用することも選択肢の一つです。ただし、これは経過時間を測定するためのAPIであり、現在時刻を取得するものではないため、用途が限られます:
// 処理時間の計測などに使用
const start = performance.now();
// 何らかの処理
const end = performance.now();
const processingTime = end - start;

具体的な実装例

例1: ブログ記事の投稿日時表示

// components/PostDate.tsx
"use client";

import { useEffect, useState } from 'react';
import dayjs from 'dayjs';
import relativeTime from 'dayjs/plugin/relativeTime';
import 'dayjs/locale/ja';

dayjs.extend(relativeTime);
dayjs.locale('ja');

export function PostDate({ dateString }: { dateString: string }) {
  const [formattedDate, setFormattedDate] = useState('');
  
  useEffect(() => {
    const date = dayjs(dateString);
    const now = dayjs();
    
    // 1週間以内なら相対表示、それ以外は絶対日付
    if (now.diff(date, 'day') <= 7) {
      if (now.diff(date, 'day') === 0) {
        setFormattedDate('今日');
      } else {
        setFormattedDate(date.fromNow());
      }
    } else {
      setFormattedDate(date.format('YYYY年MM月DD日'));
    }
  }, [dateString]);
  
  // 初期表示用のフォールバック
  if (!formattedDate) {
    return <time dateTime={dateString}>{dateString}</time>;
  }
  
  return <time dateTime={dateString}>{formattedDate}</time>;
}

例2: 最適化されたPostShortsコンポーネント
エラーメッセージのコードを参考に、修正版を作成します:

// src/app/PostShorts.tsx
import { formatStaticDate } from '@/lib/formatStaticDate';
import { ClientRelativeDate } from '@/components/ClientRelativeDate';

// サーバーコンポーネント
export default function PostShorts({ posts }) {
  return (
    <div className="post-list">
      {posts.map((post) => (
        <article key={post.id} className="post-short">
          <h2>{post.title}</h2>
          <div className="post-meta">
            {/* 静的な日付フォーマット(サーバーサイド) */}
            <span className="post-date" title={formatStaticDate(post.dateString)}>
              {/* 相対日付(クライアントサイド) */}
              <ClientRelativeDate dateString={post.dateString} />
            </span>
          </div>
          <p>{post.excerpt}</p>
        </article>
      ))}
    </div>
  );
}

// src/components/ClientRelativeDate.tsx
"use client";

import { useState, useEffect } from 'react';
import dayjs from 'dayjs';
import relativeTime from 'dayjs/plugin/relativeTime';
import 'dayjs/locale/ja';

dayjs.extend(relativeTime);
dayjs.locale('ja');

export function ClientRelativeDate({ dateString }: { dateString: string }) {
  const [relativeDate, setRelativeDate] = useState('');
  
  useEffect(() => {
    const date = dayjs(dateString);
    if (date.isValid()) {
      const now = dayjs();
      if (now.diff(date, "day") === 0) {
        setRelativeDate("今日");
      } else {
        setRelativeDate(date.fromNow());
      }
    }
  }, [dateString]);
  
  return <>{relativeDate || dateString}</>;
}

// src/lib/formatStaticDate.ts
import dayjs from 'dayjs';

export function formatStaticDate(dateString: string): string {
  const date = dayjs(dateString);
  if (date.isValid()) {
    return date.format('YYYY年MM月DD日');
  }
  return dateString;
}

パフォーマンスへの影響と考慮点

1. ハイドレーションコスト
クライアントコンポーネントに日付計算を移行すると、JavaScriptのダウンロードとハイドレーションが必要になります。小規模なアプリケーションでは問題ありませんが、大量の日付表示を行う場合は注意が必要です。
2. レイアウトシフト
クライアントサイドで日付が更新されると、特に「1ヶ月前」から「今日」のような変化があると、レイアウトシフトが発生する可能性があります。これを防ぐために、最初から十分なスペースを確保するCSSを設定しておくことが重要です。
3. SEO対策
検索エンジンにとって、記事の公開日時は重要な情報です。クライアントサイドだけでなく、以下のようにセマンティックなHTML要素を使用することをお勧めします:
tsx<time dateTime="2023-05-16T12:34:56Z">
<ClientRelativeDate dateString="2023-05-16T12:34:56Z" />
</time>

まとめ

SSR環境でnew Date()を使用する際の問題点と解決策について解説しました:

問題点:

ビルド時や初回レンダリング時の時刻が固定される
キャッシュされたページで古い時刻情報が表示される
ハイドレーション不一致が発生する可能性がある

解決策:

日付に関する処理をクライアントコンポーネントに移行する
静的フォーマットと動的な相対時間表示を分離する
Next.jsのconnection()APIを使用する
用途に応じてperformance.now()を検討する

ベストプラクティス:

サーバーでは固定的な日付フォーマットを使用し、相対表示はクライアントで行う
SEO対策として適切なHTML要素(<time>)を使用する
レイアウトシフトを防ぐためのCSSを設定する

Next.jsやNuxt.jsなどのモダンなフレームワークを使用する際は、SSRの特性を理解し、日付処理などの動的な要素を適切に扱うことが重要です。この記事が、より良いユーザー体験と開発体験の助けになれば幸いです。

Discussion