Article

Webフォントや画像最適化で表示速度を改善する方法

高田晃太郎
Webフォントや画像最適化で表示速度を改善する方法

HTTP Archiveの公開データでは、モバイルページの転送量の約半分を画像が占め、フォントも無視できない割合を占めています。¹ さらにDeloitteとGoogleの共同研究では、モバイルのページ速度を0.1秒改善すると小売ではコンバージョン率が平均8%向上したと報告されています。² 現場で再現性高く数値で効果が立ちやすいのは、体感ではなく、フォントと画像の徹底最適化です。LCPの多くが画像由来、CLSの多くがフォントと画像寸法由来という現実に正面から向き合い、今日から移せる実装に落とし込んでいきます。⁸⁴

計測ファーストで設計を決める

改善は計測から始まります。まずはCore Web Vitalsのうち、LCP(Largest Contentful Paint: 最大要素の描画完了)とCLS(Cumulative Layout Shift: レイアウトのズレ量)をモバイル実機前提で観測し、ラボではLighthouse、実地ではRUM(Real User Monitoring)とCrUX(Chrome UX Report)を併用します。あわせてINP(Interaction to Next Paint: ユーザー操作から次の描画までの応答)も見ておくと安全です。LCPターゲットは2.5秒未満、CLSは0.1未満、INPは200ms未満を上限に置き、ヒーロー画像とWebフォントの挙動がウォーターフォール上でどう見えるかを確認します。³ フォントはFOIT(フォントが来るまで文字が見えない)やFOUT(フォールバックからフォントに切り替わる)発生の有無、画像はLCP候補の解像度とデコードタイミング、そしてネットワーク優先度が重要です。ここで予算も定義します。ヒーロー画像は100〜200KB(AVIF/WebP時)、初期フォントは100KB未満、初回描画に関与しないリソースは遅延させるという線引きを明確にします。実装後は同一条件のThrottling(4G/CPU 4x slow)で比較し、バイト、リクエスト、LCP、CLS、INPの変化を1リリース単位で記録しておくと、回帰検知が容易になります。

Webフォント最適化の実践

最重要原則はシンプルで、サブセット化したWOFF2をpreloadし、font-displayで描画ブロックを避け、必要な重みだけを配信することです。⁵ CDN配信のGoogle Fontsは便利ですが、キャッシュ制御や安定性を優先するならセルフホストが扱いやすい場面が多いというのが実務の感触です。まずはLCPに影響する本文フォントから着手し、見出しやアイコン類は後回しにします。以下の最小構成でFOITを抑えつつ、CLSも起こさない実装に寄せます。必要に応じてfallbackとのメトリクス差を詰めるsize-adjust/ascent-override等の指定も検討するとさらに安定します(値はフォントごとに要計測)。⁵

<!-- 1) ヒーローで使う本文フォントだけを事前読み込み -->
<link rel="preload" href="/fonts/Inter-JP-regular-subset.woff2" as="font" type="font/woff2" crossorigin>
/* 2) @font-faceはWOFF2優先、unicode-rangeでサブセット化 */
@font-face {
  font-family: 'InterJP';
  src: url('/fonts/Inter-JP-regular-subset.woff2') format('woff2');
  font-weight: 400;
  font-style: normal;
  font-display: swap; /* FOIT回避、CLSも抑制 */
  unicode-range: U+0020-007E, U+3000-30FF, U+4E00-9FFF; /* ASCII + Kana + CJK */
  /* fallback整合(必要に応じて計測して設定)
     size-adjust: 100%;
     ascent-override: 90%;
     descent-override: 10%;
     line-gap-override: normal; */
}
body { font-family: 'InterJP', system-ui, -apple-system, Segoe UI, Roboto, 'Hiragino Sans', 'Noto Sans JP', sans-serif; }
# 3) サブセット化例(fonttools/pyftsubset)
# インストール: pip install fonttools brotli zopfli
pyftsubset InterJP-Regular.ttf \
  --output-file=Inter-JP-regular-subset.woff2 \
  --flavor=woff2 \
  --layout-features='*' --no-hinting --desubroutinize \
  --unicodes="U+0020-007E,U+3000-30FF,U+4E00-9FFF" \
  --with-zopfli

Next.jsを使う場合はnext/fontが安定です。自動preload、フォールバック、サブセット指定を一箇所で完結できます。⁷

// 4) Next.js 14+ の next/font 利用例(Google or local)
import { Inter } from 'next/font/google';
// import localFont from 'next/font/local'

export const inter = Inter({
  subsets: ['latin'], // 日本語は別フォントをlocalで
  display: 'swap',
  weight: ['400'],
  preload: true,
});

export default function Page() {
  return <main className={inter.className}>こんにちは</main>;
}

HTTPヘッダーでの制御も効果が大きいです。フォントは長期キャッシュとimmutable、初回はpreloadヒントを明示しておきます。

// 5) Node/ExpressでのCache-ControlとPreload
import express from 'express';
const app = express();

app.use('/fonts', (req, res, next) => {
  res.setHeader('Cache-Control', 'public, max-age=31536000, immutable');
  next();
});

app.get('/', (req, res) => {
  res.setHeader('Link', '</fonts/Inter-JP-regular-subset.woff2>; rel=preload; as=font; type="font/woff2"; crossorigin');
  res.sendFile('index.html', { root: './public' });
});

app.listen(3000);

これらの施策により、一般的な日本語サイトでも本文用フォントの総転送量が数百KBから数十KB台まで下がるケースは多く、FCPやLCPの短縮が期待できます。とくに重みの整理、サブセット、preload、displayの四点セットは基本形として有効で、適切に適用すれば初回描画の待ち時間を目に見えて抑えられます。⁵

画像最適化の実践

画像はLCPを左右します。最初にヒーロー画像の形式とサイズを見直し、適切なフォーマット(AVIF/WebP優先、フォールバックJPEG)、正しい解像度、そしてfetchpriority=highで優先度を上げます。⁶ HTMLでの実装は次のようになります。

<!-- 6) ヒーロー画像:AVIF/WebP優先、LCP対象にfetchpriority -->
<picture>
  <source type="image/avif" srcset="/img/hero-1200.avif 1200w, /img/hero-800.avif 800w" sizes="(max-width: 768px) 100vw, 1200px">
  <source type="image/webp" srcset="/img/hero-1200.webp 1200w, /img/hero-800.webp 800w" sizes="(max-width: 768px) 100vw, 1200px">
  <img src="/img/hero-1200.jpg" width="1200" height="630" alt="Product hero"
       decoding="async" fetchpriority="high" />
</picture>

Next.jsのImageコンポーネントを使う場合、priorityでLCP候補を明示し、sizesで無駄な転送を避けます。CDN側でAVIF配信が可能なら自動変換の恩恵も受けられます。

// 7) Next.js Image: LCP対象はpriority、適切なsizes
import Image from 'next/image';

export default function Hero() {
  return (
    <Image
      src="/img/hero-1200.jpg"
      alt="Product hero"
      width={1200}
      height={630}
      priority
      sizes="(max-width: 768px) 100vw, 1200px"
      placeholder="blur"
      blurDataURL="data:image/jpeg;base64,..."
    />
  );
}

配信前の最適化は自動化しておくと保守がラクになります。Sharpでソースから複数解像度とAVIF/WebPを一括生成する例を示します。

// 8) Sharpでのマルチフォーマット/解像度生成
import sharp from 'sharp';

async function buildVariants(input) {
  const sizes = [800, 1200];
  await Promise.all(sizes.map(async (w) => {
    await sharp(input).resize(w).avif({ quality: 45 }).toFile(`dist/hero-${w}.avif`);
    await sharp(input).resize(w).webp({ quality: 60 }).toFile(`dist/hero-${w}.webp`);
    await sharp(input).resize(w).jpeg({ quality: 72, mozjpeg: true }).toFile(`dist/hero-${w}.jpg`);
  }));
}

buildVariants('src/hero.png');

CLS対策として幅と高さの明示は必須です。画像のアスペクト比が予約されることで、表示中のレイアウトが安定します。⁴ Heroだけでなくリストカードのサムネイルにも寸法を与え、lazyロードのプレースホルダはぼかしや単色フィルで十分です。一般的に、同解像度ならAVIFはJPEG比で30〜50%程度の転送量削減が期待でき、WebPでも20〜30%程度になることが多いため、LCP改善に寄与しやすい領域です。⁸

配信・キャッシュ・ネットワークの整備

最適化はファイルだけで完結しません。Cache-Controlの設計、CDNでの圧縮と優先度、そして初回取得のヒントがLCP/TTFBに効きます。フォントと画像はファイル名にコンテンツハッシュを含め、immutableな長期キャッシュを付与します。CDNではBrotli圧縮を有効化し、AVIFやWebPは圧縮の二重化を避けるために正しいContent-Typeを返します。HTML側にはpreconnectで重要オリジンのTCP/TLS確立を先行させます。Early Hints(103)が使える環境であれば、フォントとLCP画像を先出ししておくとネットワークアイドルが短くなります。サービスワーカーは万能ではありませんが、画像やフォントのstale-while-revalidateは体験の一貫性に寄与します。

// 9) シンプルなService Worker: 画像とフォントをSWRでキャッシュ
self.addEventListener('fetch', (event) => {
  const url = new URL(event.request.url);
  const isAsset = url.pathname.endsWith('.woff2') || url.pathname.match(/\.(?:avif|webp|jpe?g|png)$/);
  if (!isAsset) return;
  event.respondWith((async () => {
    const cache = await caches.open('assets-v1');
    const cached = await cache.match(event.request);
    const network = fetch(event.request).then((res) => {
      cache.put(event.request, res.clone());
      return res;
    }).catch(() => cached);
    return cached || network;
  })());
});

運用においては、Lighthouse CIやPageSpeed Insights APIでビルドごとにパフォーマンスバジェットを検証します。LCPが2.5秒を超えた場合にPRをブロックするなど、品質ゲートとして扱うと回帰が起こりにくくなります。CDNログからはヒット率と転送量の推移を追い、画像のミスマッチ(非対応ブラウザにAVIFだけを返していないか)やオリジン回避率の悪化を早期に検知します。キャッシュ戦略とビルドパイプラインは常に対で管理する、この原則がチーム横断の運用を強くします。

ビジネスインパクトと実践の順序

最短で効果を出す順序は明快です。まずLCP候補のヒーロー画像に的を絞り、形式、解像度、優先度を正す。その次に本文フォントのサブセットとpreload、displayの最適化を行い、初期描画を止めない。それらが安定したら、残る画像群の自動最適化とCDNキャッシュを整備し、最後にCIで計測を日常化します。公開事例でもこの流れでLCPの大幅改善や転送量削減につながるケースが多数報告されていますが、実際の改善幅はコンテンツ構造やネットワーク条件に依存します。速度はユーザー体験であり、同時に収益構造の改善策でもあります。²

実装チェックの観点

日々の改善で見落としがちなポイントは、LCP画像のfetchpriority指定漏れ、画像のwidth/height未指定によるCLS、フォントの不要ウェイト読み込み、そしてpreloadと実リクエストURLの不一致です。ブラウザのネットワークパネルで優先度を観察し、Coverageで未使用フォントやCSSがないかを確認します。RUMのサンプリング率はユーザー規模に合わせ、少量でもLCP・CLS・INPが母集団を正しく代表するように保ちます。改善の手応えは、単発のスコアではなく、傾向線で評価するのが安全です。

まとめ

パフォーマンス改善は学術的な理想論ではなく、日々のプロダクトを動かす実務です。ヒーロー画像の最適化、本文フォントのサブセットとpreload、displayの適用、そしてキャッシュとCDNの整備という順序で進めれば、今日からでもLCPとCLSを確実に前進させられます。最小の変更で最大の効果が出る箇所に集中し、計測で裏づける、このリズムをチームの標準にしましょう。次のスプリントでまずどのページのヒーロー画像から手を付けますか。計測用のダッシュボードとビルドスクリプトを準備すれば、改善の第一歩はもう半分進んでいます。

参考文献

  1. Media — The Web Almanac 2022 (HTTP Archive). https://almanac.httparchive.org/en/2022/media
  2. Milliseconds Make Millions — Deloitte Digital and Google. https://www.readkong.com/page/milliseconds-make-millions-a-study-on-how-improvements-in-5298599
  3. Core Web Vitals — web.dev. https://web.dev/articles/vitals
  4. Optimize Cumulative Layout Shift — web.dev. https://web.dev/articles/optimize-cls/
  5. Optimize WebFont loading and rendering — web.dev. https://web.dev/articles/optimize-webfont-loading/
  6. Priority Hints (fetchpriority) — web.dev. https://web.dev/articles/fetch-priority/
  7. Optimizing Fonts — Next.js Docs. https://nextjs.org/docs/app/building-your-application/optimizing/fonts
  8. CSS and Web Vitals — web.dev. https://web.dev/articles/css-web-vitals/