Tailwind v4 + Turbopack で Google Fonts が読み込めない — next/font 自己ホスト移行で解決
Next.js 16 + Tailwind v4 + Turbopack の構成で、CSS の @import で読み込んでいた Google Fonts が本番だけ ERR_FAILED になり、フォントが全滅する問題を踏みました。この記事では、実際に起きた症状と、next/font/google の自己ホストへ移行して解決した手順(コード全文つき)、日本語(CJK)フォント特有の preload: false の注意点、そして CSP から Google のドメインを外せる副次効果までをまとめます。
何が起きたか
個人開発プロダクト Lifefolio — by JurisCodeAI の本番で、ある時からフォントだけが素の system-ui に落ちていることに気づきました。開発環境(next dev)では正常に表示されるのに、本番ビルドだけ崩れる、という嫌なパターンです。
DevTools の Network タブを見ると、fonts.googleapis.com への CSS リクエストが net::ERR_FAILED で落ちていました。当時のフォント読み込みは、Tailwind の入口になっている globals.css の先頭に書いた、よくあるこの1行です。
@import url("https://fonts.googleapis.com/css2?family=Noto+Sans+JP:wght@400;500;700&display=swap");アプリのコードは変えていないのに、ビルド環境(Tailwind v4 + Turbopack)との組み合わせでこの @import が本番で機能しなくなっていました。
なぜ CSS @import は壊れやすいか
Tailwind v4 は従来の tailwind.config.js 中心の構成から、CSS ファースト(`@theme` ディレクティブ)の構成に大きく変わりました。CSS が単なるスタイルシートではなく「ビルドの処理対象」としての性格を強めた結果、CSS 内の @import は Turbopack のパイプラインに深く関与するようになり、外部 URL の @import は従来より壊れやすい経路になっています。
そもそも外部 @import 方式には、今回の問題を別にしても弱点があります。
- レンダリングをブロックする外部リクエストが増える(CSS → フォント本体の2段階)
fonts.googleapis.com/fonts.gstatic.comを CSP の許可リストに入れ続ける必要がある- ビルド時に検証されないので、壊れてもビルドは緑のまま本番だけ崩れる
最後の点が今回の本質でした。フォントの失敗はビルドエラーにならないので、デプロイ後に実物を見るまで気づけません。
対処: next/font/google で自己ホストする
Next.js 標準の next/font/google に移行しました。ビルド時にフォントファイルをダウンロードして自分のドメインから配信(自己ホスト)する仕組みなので、実行時に Google へのリクエストが一切発生しなくなります。Lifefolio で実際に使っている layout.tsx の該当部分の全文がこちらです。
import { Noto_Sans_JP, Roboto_Mono } from "next/font/google";
// フォントはビルド時に自己ホスト(外部Googleへのリクエスト無し=CSP非依存・高速・レイアウトシフト無し)。
// Noto Sans JP は CJK のためグリフが膨大で preload 不可。
const notoSansJP = Noto_Sans_JP({
subsets: ["latin"],
weight: ["400", "500", "700", "900"],
variable: "--font-sans",
display: "swap",
preload: false,
});
const robotoMono = Roboto_Mono({
subsets: ["latin"],
weight: ["500", "700"],
variable: "--font-mono",
display: "swap",
});
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="ja" className={`${notoSansJP.variable} ${robotoMono.variable}`}>
<body>{children}</body>
</html>
);
}variable オプションで CSS 変数として公開し、globals.css 側はその変数を参照するだけにします。
body {
font-family: var(--font-sans), 'Noto Sans JP', system-ui, sans-serif;
}
.mono {
font-family: var(--font-mono), 'Roboto Mono', monospace;
}これで壊れていた外部 @import は削除できます。フォントはビルド成果物に含まれるので、「ビルドが通ればフォントも配信される」という当たり前の状態になりました。
CJK フォントの注意点: preload は false にする
日本語フォントで1つだけハマりどころがあります。Noto Sans JP のような CJK フォントは `preload: false` にすることです。
next/font の preload は、使用するサブセットのフォントファイルを <link rel="preload"> で先読みする仕組みですが、CJK はグリフ数が膨大でファイルが大きく、Next.js が preload 対象にできる subsets 指定にも日本語全体は収まりません。preload: true(既定)のまま CJK を使うと、ビルド時に警告が出たり、巨大ファイルの先読みでかえって初期表示を圧迫したりします。display: "swap" と組み合わせて「まず表示、フォントは追って適用」にするのが実用的でした。英数フォント(Inter や Roboto Mono など)は小さいので preload ありのままで問題ありません。
副次効果: CSP から Google ドメインを外せる
自己ホスト化の思わぬ収穫が Content-Security-Policy の簡素化です。移行前は style-src に fonts.googleapis.com、font-src に fonts.gstatic.com を許可し続ける必要がありましたが、フォントが自分のドメインから配信されるようになったので、この2つを CSP から丸ごと削除できました。許可する外部オリジンが減るぶん、攻撃面も設定の複雑さも減ります。
もう1つ、Turbopack 環境では next/font のモジュール解決が連続ビルドで間欠的に失敗する(Can't resolve '@vercel/turbopack-next/internal/font/google/font' が出たり出なかったりする)事象にも遭遇しました。こちらは検証スクリプトに rm -rf .next を組み込んでクリーンビルドを強制することで安定しています。
まとめ: チェックリスト
Tailwind v4 + Turbopack の構成でフォントを扱うなら、次を確認しておくことをおすすめします。
- CSS の外部
@importによる Google Fonts 読み込みは本番だけ壊れても気づけない(ビルドは緑のまま) next/font/googleの自己ホストに移行し、variableで CSS 変数化してglobals.cssから参照する- CJK フォントは `preload: false` + `display: "swap"`。英数フォントは preload ありでよい
- 移行後は CSP から
fonts.googleapis.com/fonts.gstatic.comを削除できる - Turbopack でフォント解決が間欠的に失敗する場合は、検証フローで
rm -rf .nextのクリーンビルドを強制する
「ビルドは緑なのに本番だけ壊れる」系の障害は、無料プランの自動停止で本番が落ちた「Supabase 無料プランの自動一時停止で本番が落ちた話」でも書きました。個人開発では検知の仕組みごと仕込んでおくのが、結局いちばん安上がりだと思います。
#Next.js#Tailwind CSS#Turbopack#個人開発