前回の第2回では、Next.jsでのCSSスタイリング手法(Tailwind CSS, CSS Modules, clsx)について解説しました。
連載第3回となる今回は、Learn Next.js Chapter 3「Optimizing Fonts and Images」の内容をまとめていきます。
Webサイトのパフォーマンスとユーザー体験(UX)に直結する「フォント」と「画像」の最適化。Next.jsでは、専門的な知識がなくても、公式のコンポーネントを使うだけで自動的に最適化してくれます。本記事では、その仕組みと具体的な実装方法について学んでいきましょう!
本記事で学ぶこと
- Next.jsがフォントと画像を最適化する理由(CLS対策・パフォーマンス向上)
- next/font を使ったGoogleフォントの追加と最適化
- next/image コンポーネントを使った画像の追加と自動最適化
- 公式クイズと練習問題の解答・解説
1. なぜフォントと画像を最適化するのか?
Cumulative Layout Shift (CLS) とは?
フォントや画像が原因で発生する、Webサイトの「カクッ」としたレイアウト崩れ(予期せぬ要素の移動)を数値化した指標を CLS(Cumulative Layout Shift) と呼びます。
例えば、フォントの読み込みでは以下のようなことが起こります。
- ブラウザが最初にシステムフォント(代替フォント)でテキストを表示する。
- カスタムフォント(Googleフォントなど)のダウンロードが完了する。
- フォントが切り替わり、文字のサイズや間隔が変わって周りの要素がズレる(レイアウトシフト)。
これは、ユーザーにとって非常に不快な体験であり、GoogleのSEO評価(Core Web Vitals)にも悪影響を与えます。
Next.jsの解決策:next/font
Next.jsでは、next/font モジュールを使うことで、この問題を自動的に解決します。
- ビルド時にフォントファイルをダウンロード:
アプリケーションのビルド時にフォントファイルをダウンロードし、他の静的資産(画像など)と一緒にホストします。 - 追加のネットワークリクエストが不要:
ユーザーがサイトを訪問した際、ブラウザはGoogle Fontsのサーバーへ追加でリクエストを送る必要がないため、フォントの読み込みが高速になり、CLSを防止できます。
フォントデータも一緒に送られてくるなら、最初の読み込みが重くなるのでは?と自分は考えたこともあります。
しかし、以下の理由で全体としてむしろ軽くなり、体感速度は圧倒的に速くなります。1. サブセット化(Subset)でデータ量を極限まで削っている
Google Fontsなどのフォントファイル全体(全漢字・特殊文字など)は数メガバイト(MB)あり非常に重いです。
しかし、Next.jsは必要な文字だけを切り取った(サブセット化した)極小のフォントファイルを自動生成します。 これにより、本来数MBあるフォントデータが わずか数キロバイト(KB〜数十KB) まで軽量化されます。
2. 「通信の立ち上げコスト(DNS・TLSハンドシェイク)」が消える
Webサイトの読み込みで時間がかかる大きな原因は、ファイルの容量(KB数)そのものよりも「新しいサーバーに接続を確立する時間」です。
従来の方式だと、ブラウザは以下の手順を踏むため何度も通信待ちが発生していました。
- 自サーバーに接続してHTMLを取得
- Google FontsのサーバーにDNS問い合わせ + SSL/TLS暗号化接続の確立(ここでタイムロス)
- フォントファイルをリクエストして取得
next/font では自サーバー(同一ドメイン)から一括で取れるため、接続のやり取り(ハンドシェイク)が1回で済み、結果的に待ち時間が劇的に短縮されます。
3. WOFF2形式への自動変換とインラインCSS化
next/font は、現在最も圧縮率が高い WOFF2形式 を自動採用します。 さらに、フォントを読み込むための @font-face というCSS定義自体もHTMLの <head> 内に直接埋め込む(インライン化する)ため、CSSファイルのダウンロード待ちすら発生しません。

❓ 公式クイズ 1
Next.jsはどのようにフォントを最適化しますか?
正解:D(フォントファイルを他の静的資産と一緒にホストし、追加のネットワークリクエストが発生しないようにする)
2. 実践:メインフォント(Inter)を追加する
それでは、実際にGoogle Fontsの「Inter」フォントをアプリ全体に適用してみましょう。
step 1: /app/ui/fonts.ts を作成する
インポート用の共通ファイルを作成し、Interフォントをセットアップします。
// app/ui/fonts.ts
import { Inter } from 'next/font/google';
// プライマリフォントとしてInterをインポート。サブセット(対象言語)に 'latin' を指定
export const inter = Inter({ subsets: ['latin'] });step 2: /app/layout.tsx で適用する
ルートレイアウトの <body> タグに、Interフォントのクラス名を指定します。
// app/layout.tsx
import '@/app/ui/global.css';
import { inter } from '@/app/ui/fonts'; // ★インポート
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
{/* ★bodyタグにinter.classNameと、Tailwindのantialiasedクラスを適用 */}
<body className={`${inter.className} antialiased`}>{children}</body>
</html>
);
}これで、アプリ全体に Inter フォントが適用されます。antialiased クラスは必須ではありませんが、フォントの縁を滑らかにするTailwindのユーティリティで、デザイン性が上がります。
3. 練習問題:セカンダリフォント(Lusitana)を追加する
今度は、特定の要素だけ別のフォントを適用する練習です。
練習内容
- fonts.ts に「Lusitana」フォント(weight: 400, 700)を追加。
- /app/page.tsx の <p> タグと <AcmeLogo/> に適用。
解答例
まずは fonts.ts を更新します。
// app/ui/fonts.ts
import { Inter, Lusitana } from 'next/font/google'; // ★Lusitanaを追加
export const inter = Inter({ subsets: ['latin'] });
// ★Lusitanaをセットアップ。weightを指定
export const lusitana = Lusitana({
weight: ['400', '700'],
subsets: ['latin'],
});次に page.tsx でインポートして適用します。
// app/page.tsx
import AcmeLogo from '@/app/ui/acme-logo';
// ...
import { lusitana } from '@/app/ui/fonts'; // ★インポート
export default function Page() {
return (
<main className="flex min-h-screen flex-col p-6">
<div className="flex h-20 shrink-0 items-end rounded-lg bg-blue-500 p-4 md:h-52">
{/* ★コメントアウトを解除して適用 */}
<AcmeLogo />
{/* ... */}
</div>
<div className="mt-4 flex grow flex-col gap-4 md:flex-row">
<div className="flex flex-col justify-center gap-6 rounded-lg bg-gray-50 px-6 py-10 md:w-2/5 md:px-20">
{/* ... */}
{/* ★pタグにlusitana.classNameを適用 */}
<p className={`${lusitana.className} text-xl text-gray-800 md:text-3xl md:leading-normal`}>
<strong>Welcome to Acme.</strong> This is the example for the{' '}
{/* ... */}
</p>
{/* ... */}
</div>
{/* ... */}
</div>
</main>
);
}4. なぜ画像を最適化するのか?(next/imageの解決策)
従来の HTML の <img> タグでは、以下の点を手動で実装する必要がありました。
- レスポンシブ対応(画面サイズに応じた画像サイズの変更)
- レイアウトシフト(CLS)の防止
- 遅延読み込み(Lazy loading:ビューポートに入ってから読み込む)
- モダンな画像形式(WebP, AVIF)への変換
これらの複雑な最適化を、単一のコンポーネントで自動化してくれるのが Next.jsの <Image> コンポーネント(next/image)です。
- レイアウトシフト防止:
画像のサイズ(width, height)に基づいてアスペクト比を計算し、読み込み前に表示領域を確保します。 - レスポンス画像生成:
デバイスに応じて画像を自動でリサイズし、配信します。 - デフォルトで遅延読み込み:
ビューポートに入る直前まで画像の読み込みを遅らせます。
「もし next/image を使わずに自力でやろうとしたら…」
正直、「単なる <img> タグのラッパーでしょ?わざわざ専用コンポーネントを使う意味ある?」と思っていました。
しかし <Image> を使わずに、現代のWebで求められる最高レベルの画像最適化(表示速度向上・CLS防止・レスポンシブ・次世代フォーマット対応)を自力でやろうとすると、たった1枚の画像を表示するためだけに余計な HTML を書くハメになります。
しかも、コードを書く以前に下準備として、大量の画像生成作業が必要になります。
元画像1枚から、PC用・スマホ用、さらにはRetina(高精細)ディスプレイ用、WebPやAVIFへのフォーマット変換版など、1枚の画像につき何パターンも手動でリサイズ・書き出ししなければなりません。

❓ 公式クイズ 2
正誤問題:サイズ指定のない画像とWebフォントは、レイアウトシフトの一般的な原因である。
正解:A(True)
5. 実践:デスクトップ用ヒーロー画像を追加する
それでは、<Image> コンポーネントを使って、トップページにヒーロー画像を表示してみましょう。
画像のソースファイルは /public フォルダ直下に配置します(チュートリアルではすでに用意されています)。
/app/page.tsx を編集し、<Image> コンポーネントをインポートして適用します。
// app/page.tsx
import AcmeLogo from '@/app/ui/acme-logo';
// ...
import { lusitana } from '@/app/ui/fonts';
import Image from 'next/image'; // ★1. インポート
export default function Page() {
return (
// ...
<main className="flex min-h-screen flex-col p-6">
{/* ... */}
<div className="mt-4 flex grow flex-col gap-4 md:flex-row">
{/* ...テキストエリア... */}
{/* ★2. ヒーロー画像エリアに<Image>コンポーネントを追加 */}
<div className="flex items-center justify-center p-6 md:w-3/5 md:px-28 md:py-12">
{/* Add Hero Images Here */}
<Image
src="/hero-desktop.png" // 画像パス
width={1000} // アスペクト比計算用の幅(実際の表示サイズではない)
height={760} // アスペクト比計算用の高さ
className="hidden md:block" // Tailwind: モバイルでは隠し、md画面以上で表示
alt="Screenshots of the dashboard project showing desktop version"
/>
</div>
</div>
</main>
);
}画像サイズ(width, height)の指定方法
<Image> コンポーネントでは、アスペクト比を決定するために、元画像の幅と高さを指定する必要があります。これは、実際の表示サイズを決定するものではなく、レイアウトシフトを防止するための非常に重要なステップです。
アスペクト比(Aspect Ratio)とは、画像の「縦と横の長さの比率(縦横比)」のことです。
Next.js が画像を描画する際、画像ファイルを完全にダウンロードし終わる前でも、「この画像がどんな形(縦横比)をしているか」 を把握する必要があります。
この比率(1000 : 760)さえ分かれば、画面幅に合わせて画像がどれだけ縮小・拡大されても、画像を読み込む前に正確な「表示スペース(枠)」をブラウザ上に確保できます。
【画像読み込み前】
┌────────────────────────┐
│ │ ← アスペクト比から計算して
│ 枠だけ先に確保 │ 先に「空きスペース」を作っておく
│ │
└────────────────────────┘
【画像読み込み完了】
┌────────────────────────┐
│ │
│ きれい画像が入る │ ← 周りの文章やボタンの位置が
│ │ 1ピクセルもズレない!(CLS防止)
└────────────────────────┘つまり、width や height に入れる数値は「絶対そのピクセルサイズで表示する」という意味ではなく、「画像のアスペクト比(形)を正確に伝えるためのメジャー(物差し)」 の役割を果たしています。
6. 練習問題:モバイル用ヒーロー画像を追加する
今度は、モバイル端末で見ている時に表示する、別の画像を追加する練習です。
練習内容
- hero-desktop.png と同じ publicフォルダに hero-mobile.png があることを確認。
- サイズ: width=560, height=620。
- Tailwindのクラスを使い、モバイルで表示・デスクトップで非表示にする。
解答例
// app/page.tsx
// ...
<div className="mt-4 flex grow flex-col gap-4 md:flex-row">
{/* ...テキストエリア... */}
<div className="flex items-center justify-center p-6 md:w-3/5 md:px-28 md:py-12">
{/*Add Hero Images Here */}
{/* デスクトップ用画像 */}
<Image
src="/hero-desktop.png"
width={1000}
height={760}
className="hidden md:block"
alt="Screenshots of the dashboard project showing desktop version"
/>
{/* ★モバイル用画像を追加 */}
<Image
src="/hero-mobile.png"
width={560} // アスペクト比計算用のサイズ
height={620} // アスペクト比計算用のサイズ
className="block md:hidden" // Tailwind: デフォルトで表示、md画面以上で隠す
alt="Screenshots of the dashboard project showing mobile version"
/>
</div>
</div>
// ...モバイルとデスクトップの切り替え(条件判断)を行っているのは、各 <Image> タグの className に指定された Tailwind CSS のクラス です。
具体的には、以下の部分です。
1. デスクトップ用画像 (hero-desktop.png)
className="hidden md:block"- hidden: 基本(スマホ画面サイズ)では 非表示(display: none;) にする。
- md:block: 画面幅が md(768px以上、PCサイズ)になったら 表示(display: block;) に切り替える。
2. モバイル用画像 (hero-mobile.png)
className="block md:hidden"- block: 基本(スマホ画面サイズ)では 表示(display: block;) にする。
- md:hidden: 画面幅が md(768px以上、PCサイズ)になったら 非表示(display: none;) に切り替える。
補足:JavaScriptやNext.jsではなく「CSS(メディアクエリ)」で判断している
JavaScript側で if (isMobile) のような条件分岐を行っているのではなく、CSS(Tailwindのレスポンシブブレイクポイント)を使ってブラウザの画面幅(CSSメディアクエリ)で表示・非表示を切り替えています。
まとめと次回予告
今回は、Next.js公式チュートリアル第3章をもとに、フォントと画像の最適化について解説しました。
- next/font: ビューポート外へのネットワークリクエストを防ぎ、レイアウトシフト(CLS)を防止。
- <Image> コンポーネント: サイズ調整、遅延読み込み、モダン形式への変換を自動化し、パフォーマンスを向上。
これらのコンポーネントを積極的に使うことで、Next.jsの強力な最適化機能を最大限に活用できます。
次回(第4回)は、「レイアウトとページの作成」について学んでいきます。Next.jsの特徴である「ネストされたレイアウト」の仕組みを理解し、ダッシュボードアプリの構造を作り始めていきましょう!


コメント