既存プロジェクトとデザインシステムを統一するため、内部開発のアプリのフォントをInterからNoto Sans JPに変更する必要がありました。

最初はGoogle Fonts CDN方式を使用していましたが、実装を進める中で2つの課題が浮かび上がりました。

1つ目は、CSP(Content Security Policy)の設定が複雑化していたこと。外部フォントサービスのドメインを許可リストに追加する必要があり、セキュリティ設定の管理が煩雑でした。

2つ目は、外部サービスへの依存です。Google Fontsがダウンのときにフォントが読み込めないリスクや、通信遅延がページロード時間に影響を与える懸念がありました。

そこで、Next.jsのnext/fontによるセルフホスティング方式に移行しました。実装してみると、これは想像以上にシンプルで、パフォーマンスとセキュリティの両面で大きな改善が実現できました。

こんな人におすすめ

この記事は以下のような方に役立ちます。

  • Next.js で日本語フォント(Noto Sans JP)をセットアップしたい方
  • Google Fonts CDN への外部依存を減らしたい方
  • Tailwind CSS v4 を使用していて、フォント設定を統合したい方
  • CSP(Content Security Policy)を厳しく設定している方
  • フォント周りのパフォーマンス最適化に関心のある方

要件

  • InterフォントをNoto Sans JPに置き換え
  • Google Fonts CDNへの外部リクエストを削除
  • Tailwind CSS v4の@themeブロックとの統合
  • CSP(Content Security Policy)の最適化

実装方針

  1. next/font/googleからNoto Sans JPをインポート
  2. フォント設定オブジェクトを作成(weights, display, variable)
  3. <html>要素にCSS変数を適用
  4. <body>要素にfont-sansクラスを追加
  5. globals.cssからCDN importを削除し、@themeブロックを更新
  6. next.config.tsのCSPから外部フォントドメインを削除

ポイント

next/fontの設定

import { Noto_Sans_JP } from 'next/font/google';

const notoSansJP = Noto_Sans_JP({
  subsets: ['latin'],  // 最小限のプリロード(日本語は自動で含まれる)
  weight: ['400', '500', '600', '700'],
  display: 'swap',     // FOUT防止
  variable: '--font-noto-sans-jp',
});

// htmlにCSS変数を適用
<html lang="ja" className={notoSansJP.variable}>

Tailwind CSS v4との統合

@theme {
  --font-sans: var(--font-noto-sans-jp), -apple-system, BlinkMacSystemFont,
    'Segoe UI', Roboto, sans-serif;
}

CSPの簡素化

セルフホスティングにより、以下のドメイン許可を削除できた:

  • https://fonts.googleapis.com(style-src)
  • https://fonts.gstatic.com(font-src)

学び

  • next/fontはビルド時にフォントをダウンロードし、自動的に最適化・セルフホスティングする
  • subsets: ['latin']は最小限のプリロード設定。日本語文字はnext/fontが自動的に処理する
  • CSS変数を使うことで、Tailwind CSSのユーティリティクラスとシームレスに統合できる
  • 外部フォントサービスへの依存を削除することで、CSPがシンプルになりセキュリティが向上する

得られた効果

この実装により、以下の改善が実現できました。

即時の効果:

  • CSP ルールの削減: Google Fonts に関連する 2 つのドメイン許可を削除できました
    • https://fonts.googleapis.com(style-src)
    • https://fonts.gstatic.com(font-src)
  • セキュリティの強化: 外部サービスへの依存がなくなり、セキュリティポリシーがシンプルになりました

パフォーマンス面での改善:

  • First Contentful Paint(FCP)の改善: フォントがビルド時にダウンロードされるため、ページロード時の外部通信が削減されました
  • 安定性の向上: Google Fonts サービスの可用性に依存しなくなり、確実にフォントが読み込まれるようになりました

開発効率の向上:

  • セルフホスティングにより、フォント設定が一箇所に集約されて管理が楽になりました
  • Tailwind CSS との統合がシームレスで、デザインシステムの保守性が向上しました

トラブルシューティング

フォントが正しく読み込まれない

現象: ビルド後にフォントが適用されず、フォールバックフォントが表示される

原因と解決策:

  1. subsetsの指定ミス

    // ❌ 誤った指定
    subsets: ['japanese']  // この指定は不要
    
    // ✅ 正しい指定
    subsets: ['latin']  // 日本語は自動で含まれる
    
  2. 環境変数の未設定

    # 環境変数が正しく設定されているか確認
    echo $NODE_ENV  # production であることを確認
    
    # 開発環境ではnext devで確認
    npm run dev
    
  3. キャッシュの問題

    # Next.jsキャッシュをクリア
    rm -rf .next
    npm run build
    

CSS変数が適用されない

現象: Tailwind CSSのfont-sansが適用されない

解決策:

  1. html要素のクラス確認

    <!-- html要素にfontクラスが適用されているか確認 -->
    <html lang="ja" class="font-sans">
    
  2. globals.cssの@themeブロック確認

    @theme {
      --font-sans: var(--font-noto-sans-jp), -apple-system, BlinkMacSystemFont,
        'Segoe UI', Roboto, sans-serif;
    }
    

ビルドサイズが大きくなる

現象: フォントファイルのためにビルドサイズが増加する

解決策:

  1. 必要なweightのみを指定

    const notoSansJP = Noto_Sans_JP({
      weight: ['400', '500', '700'],  // 実際に使用するweightのみ
      // ...
    });
    
  2. preloadの無効化(必要に応じて)

    const notoSansJP = Noto_Sans_JP({
      display: 'swap',
      variable: '--font-noto-sans-jp',
      preload: false,  // 明示的にpreloadを無効化
    });
    

FOUT(Flash of Unstyled Text)が発生する

現象: フォント読み込み中に一時的にフォールバックフォントが表示される

解決策:

  1. displayオプションの確認

    const notoSansJP = Noto_Sans_JP({
      display: 'swap',  // FOUTを許容して読み込み速度を優先
      // display: 'block' // 完全な読み込みを待つ場合(遅延が発生)
    });
    
  2. font-display CSSの確認
    カスタムCSSでフォントの表示動作を調整:

    @font-face {
      font-family: 'Noto Sans JP';
      font-display: swap;
    }
    

よくある質問(FAQ)

Q1. Google Fontsとセルフホスティング、どちらを選ぶべきですか?

A: プロジェクトの要件に応じて選択してください:

Google Fontsを選ぶべきケース:

  • プロトタイプや開発初期段階
  • 多言語対応が必要でフォントファイルサイズが大きい場合
  • CDN経由でフォントを配信する利便性を重視する場合

セルフホスティングを選ぶべきケース:

  • セキュリティポリシー(CSP)を厳しく設定している場合
  • 外部サービスへの依存を排除したい場合
  • パフォーマンス最適化を重視する場合
  • オフライン環境でのフォント表示が必要な場合

Q2. サブセット(subsets)の指定は必要ですか?

A: 基本的には不要です。subsets: ['latin'] を指定するだけで、next/font が自動的に日本語文字を含めてくれます。日本語のみのプロジェクトでも latin サブセットを指定することがベストプラクティスです。

Q3. フォントファイルのサイズはどのくらいになりますか?

A: フォントのweight(太さ)の数によって異なりますが、一般的に:

  • 1つのweight: 約 200-400KB
  • 4つのweight(400, 500, 600, 700): 約 1-1.5MB
  • すべてのweight: 約 3-4MB

実際のサイズは npm run build 後の .next フォルダ内で確認できます。

Q4. セルフホスティングすると、本当にパフォーマンスが向上しますか?

A: はい、以下の理由でパフォーマンスが向上します:

  • First Contentful Paint (FCP) の改善: フォントがビルド時にバンドルされるため、ページロード時の外部リクエストが削減されます
  • TTFBの改善: 外部CDNへの接続時間がなくなるため
  • キャッシュ効率: ユーザーがサイトを再訪問した際、ブラウザキャッシュからフォントが読み込まれます

ただし、初回ロード時のファイルサイズが増加するため、キャッシュ戦略が重要になります。

Q5. 複数のフォントを同時にセルフホスティングできますか?

A: はい、複数のフォントを同時に使用できます。例えば:

import { Noto_Sans_JP } from 'next/font/google';
import { Noto_Serif_JP } from 'next/font/google';

const notoSansJP = Noto_Sans_JP({ ... });
const notoSerifJP = Noto_Serif_JP({ ... });

// Tailwind CSSでそれぞれのフォントを定義
@theme {
  --font-sans: var(--font-noto-sans-jp), sans-serif;
  --font-serif: var(--font-noto-serif-jp), serif;
}

Q6. next/fontはNext.js 13以降でも使用できますか?

A: はい、Next.js 13以降(App Router)でも使用できます。設定方法は基本的に同じですが、フォントファイルの最適化がさらに強化されています。

Q7. フォントのライセンスはどうなっていますか?

A: Noto FontsはApache License 2.0で提供されており、商用利用も可能です。詳細はGoogle FontsのNoto Sans JPページで確認してください。

Q8. セルフホスティングのデメリットはありますか?

A: 主なデメリットは以下の通りです:

  • ビルドサイズの増加: フォントファイルがバンドルされる
  • バージョン管理: フォントの更新時に手動で対応が必要
  • CDNとのトレードオフ: キャッシュ戦略を考慮する必要がある

まとめ

Next.jsのnext/fontを使ったNoto Sans JPのセルフホスティングは、シンプルな設定でパフォーマンスとセキュリティの両面で大きなメリットをもたらします。

実装のポイント:

  1. subsets: ['latin']で最小限のプリロード
  2. CSS変数を使った柔軟なフォント管理
  3. Tailwind CSS v4の@themeブロックとの統合
  4. CSPの簡素化によるセキュリティ向上

得られる効果:

  • パフォーマンスの向上(FCP改善、外部通信削減)
  • セキュリティの強化(外部依存の排除)
  • 開発効率の向上(設定の一元化)

この手法は、日本語フォントを使用するNext.jsプロジェクトであればほぼそのまま適用できる汎用的なソリューションです。プロジェクトの要件に合わせて、ぜひ試してみてください。

参考リンク