フォント フォールバックのフレームワーク ツール

Janicklas Ralph James 氏
Janicklas Ralph James

font-display: swap を使用してフォントを読み込むサイトでは、多くの場合、ウェブフォントが読み込まれて代替フォントとスワップされる際にレイアウト シフト(CLS)が発生します。

CLS を回避するには、代替フォントのサイズをプライマリ フォントのサイズと一致させます。@font-face ルールの size-adjustascent-overridedescent-overrideline-gap-override などのプロパティを使用すると、フォールバック フォントの指標をオーバーライドできます。これにより、デベロッパーはフォントの表示方法をより細かく制御できます。フォントのフォールバックとオーバーライド プロパティについて詳しくは、こちらの投稿をご覧ください。この手法の実用的な実装は、こちらのデモでもご確認いただけます。

この記事では、Next.js と Nuxt.js のフレームワークでフォントサイズの調整を実装し、代替フォントの CSS を生成して CLS を削減する方法について説明します。また、Fontaine や Capsize などのクロスカット ツールを使用して代替フォントを生成する方法も紹介します。

背景

font-display: swap は通常、FOIT(非表示テキストのフラッシュ)を防ぎ、コンテンツを画面上により速く表示するために使用されます。swap の値を指定すると、フォントを使用するテキストはシステム フォントを使用してすぐに表示され、カスタム フォントの準備ができた場合にのみシステム フォントを置き換えるようにブラウザに指示します。

swap の最大の問題は、2 つのフォントの文字サイズの違いにより、画面コンテンツがずれて表示されるという不快感です。そのため、特にテキストが多すぎるウェブサイトでは、CLS スコアが低下します。

次の画像は問題の例を示しています。最初の画像では font-display: swap を使用していますが、代替フォントのサイズの調整は試行されていません。2 つ目の画面は、CSS の @font-face ルールを使用してサイズを調整することで、読み込みエクスペリエンスがどのように改善するかを示しています。

フォントサイズを調整しない

body {
  font-family: Inter, serif;
}
フォントやサイズが急に変わって不快感を与えるテキスト。

フォントサイズを調整した後

body {
  font-family: Inter, fallback-inter, serif;
  }

@font-face {
  font-family: "fallback-inter";
  ascent-override: 90.20%;
  descent-override: 22.48%;
  line-gap-override: 0.00%;
  size-adjust: 107.40%;
  src: local("Arial");
}
別のフォントにスムーズに切り替わるテキスト。

代替フォントのサイズを調整することは、フォント読み込みレイアウト シフトを防ぐうえで効果的な方法ですが、フォントのフォールバックに関するこちらの投稿で説明されているように、ロジックをゼロから実装するのは難しい場合があります。幸い、アプリを開発する際にこれを簡単に行えるツール オプションがいくつか用意されています。

Next.js を使用してフォントのフォールバックを最適化する方法

Next.js には、代替フォントの最適化を有効にする組み込みの方法が用意されています。この機能は、@next/font コンポーネントを使用してフォントを読み込む際に、デフォルトで有効になります。

@next/font コンポーネントは、Next.js バージョン 13 で導入されました。このコンポーネントには、Google Fonts またはカスタム フォントをページにインポートするための API が用意されており、フォント ファイルの自動自己ホスティングが組み込まれています。

使用すると、代替フォントの指標が自動的に計算され、CSS ファイルに挿入されます。

たとえば Roboto フォントを使用する場合、通常は CSS で次のように定義します。

@font-face {
  font-family: 'Roboto';
  font-display: swap;
  src: url('/fonts/Roboto.woff2') format('woff2'), url('/fonts/Roboto.woff') format('woff');
  font-weight: 700;
}

body {
  font-family: Roboto;
}

next/font に移行するには:

  1. 「next/font」から「Roboto」関数をインポートして、Roboto フォント宣言を JavaScript に移動します。関数の戻り値は、コンポーネント テンプレートで利用できるクラス名になります。この機能を有効にするには、必ず構成オブジェクトに display: swap を追加してください。

     import { Roboto } from '@next/font/google';
    
    const roboto = Roboto({
      weight: '400',
      subsets: ['latin'],
      display: 'swap' // Using display swap automatically enables the feature
    })
    
  2. コンポーネントで、生成されたクラス名 javascript export default function RootLayout({ children }: { children: React.ReactNode; }) { return ( <html lang="en" className={roboto.className}> <body>{children}</body> </html> ); } を使用します。

adjustFontFallback 構成オプション:

@next/font/google の場合: Cumulative Layout Shift の削減のために自動代替フォントを使用するかどうかを設定するブール値。デフォルトは true です。Next.js は、フォントタイプ(Serif か Sans Serif か)に応じて、代替フォントを Arial または Times New Roman に自動的に設定します。

@next/font/local の場合: Cumulative Layout Shift の削減のために自動代替フォントを使用するかどうかを設定する、文字列またはブール値の false 値。指定できる値は ArialTimes New Roman、または false です。デフォルトは Arial です。セリフ フォントを使用する場合は、この値を Times New Roman に設定することを検討してください。

Google Fonts のもう一つの選択肢は

next/font コンポーネントを使用できない場合は、optimizeFonts フラグを使って Google Fonts でこの機能を使用する方法もあります。Next.js では optimizeFonts 機能がすでにデフォルトで有効になっています。この機能は、HTML レスポンスに Google Font CSS をインライン化します。さらに、次のスニペットに示すように、next.config.js で experimental.adjustFontFallbacksWithSizeAdjust フラグを設定すると、フォント フォールバック調整機能を有効にできます。

// In next.config.js
module.exports = {
 experimental: {
   adjustFontFallbacksWithSizeAdjust: true,
 },
}

: 新しく導入された app ディレクトリでは、この機能をサポートする予定はありません。長期的には、next/font を使用することをおすすめします。

Nuxt を使用してフォント フォールバックを調整する方法

@nuxtjs/fontaine は Nuxt.js フレームワークのモジュールです。代替フォントの指標値を自動的に計算して、代替の @font-face CSS を生成します。

モジュール構成に @nuxtjs/fontaine を追加して、モジュールを有効にします。

import { defineNuxtConfig } from 'nuxt'

export default defineNuxtConfig({
  modules: ['@nuxtjs/fontaine'],
})

Google Fonts を使用している場合、またはフォントに @font-face 宣言がない場合は、追加のオプションとして宣言できます。

ほとんどの場合、モジュールは CSS から @font-face ルールを読み、フォント ファミリー、代替フォント ファミリー、表示タイプなどの詳細を自動的に推測できます。

モジュールで検出できない場所にフォントが定義されている場合は、次のコード スニペットに示すように指標情報を渡すことができます。

export default defineNuxtConfig({
  modules: ['@nuxtjs/fontaine'],
  fontMetrics: {
  fonts: ['Inter', { family: 'Some Custom Font', src: '/path/to/custom/font.woff2' }],
},
})

このモジュールが自動的に CSS をスキャンし、@font-face 宣言を読み取り、代替の @font-face ルールを生成します。

@font-face {
  font-family: 'Roboto';
  font-display: swap;
  src: url('/fonts/Roboto.woff2') format('woff2'), url('/fonts/Roboto.woff') format('woff');
  font-weight: 700;
}
/* This will be generated. */
@font-face {
  font-family: 'Roboto override';
  src: local('BlinkMacSystemFont'), local('Segoe UI'), local('Roboto'), local('Helvetica Neue'),
    local('Arial'), local('Noto Sans');
  ascent-override: 92.7734375%;
  descent-override: 24.4140625%;
  line-gap-override: 0%;
}

これで、次の例に示すように、CSS で Roboto override を代替フォントとして使用できるようになりました。

:root {
  font-family: 'Roboto';
  /* This becomes */
  font-family: 'Roboto', 'Roboto override';
}

自分で CSS を生成する

スタンドアロンのライブラリでは、代替フォントサイズ調整用の CSS を生成することもできます。

フォンテーヌ ライブラリの使用

Nuxt や Next.js を使用していない場合は、Fontaine を使用できます。Fontaine は、@nuxtjs/fontaine を支える基盤となるライブラリです。プロジェクトでこのライブラリを使用すると、Vite プラグインまたは Webpack プラグインを使用してフォールバック フォント CSS を自動的に挿入できます。

CSS ファイルに Roboto フォントが定義されているとします。

@font-face {
  font-family: 'Roboto';
  font-display: swap;
  src: url('/fonts/Roboto.woff2') format('woff2'), url('/fonts/Roboto.woff') format('woff');
  font-weight: 700;
}

Fontaine には ViteWebpack の Transformer が用意されており、ビルドチェーンに簡単に接続できます。次の JavaScript に示すように、プラグインを有効にします。

import { FontaineTransform } from 'fontaine'

const options = {
  fallbacks: ['BlinkMacSystemFont', 'Segoe UI', 'Helvetica Neue', 'Arial', 'Noto Sans'],
  // You may need to resolve assets like `/fonts/Roboto.woff2` to a particular directory
  resolvePath: (id) => 'file:///path/to/public/dir' + id,
  // overrideName: (originalName) => `${name} override`
  // sourcemap: false
}

Vite を使用している場合は、次のようにプラグインを追加します。 javascript // Vite export default { plugins: [FontaineTransform.vite(options)] }

Webpack を使用する場合は、次のように有効にします。

// Webpack
export default {
  plugins: [FontaineTransform.webpack(options)]
}

モジュールが自動的にファイルをスキャンし、@font-face ルールを変更します。 css @font-face { font-family: 'Roboto'; font-display: swap; src: url('/fonts/Roboto.woff2') format('woff2'), url('/fonts/Roboto.woff') format('woff'); font-weight: 700; } /* This will be generated. */ @font-face { font-family: 'Roboto override'; src: local('BlinkMacSystemFont'), local('Segoe UI'), local('Roboto'), local('Helvetica Neue'), local('Arial'), local('Noto Sans'); ascent-override: 92.7734375%; descent-override: 24.4140625%; line-gap-override: 0%; }

CSS の代替フォントとして Roboto override を使用できるようになりました。 css :root { font-family: 'Roboto'; /* This becomes */ font-family: 'Roboto', 'Roboto override'; }

Capsize ライブラリの使用

Next.js、Nuxt、Webpack、Vite を使用しない場合は、Capsize ライブラリを使用してフォールバック CSS を生成する方法もあります。

新しい createFontStack API

この API は、createFontStack という @capsize/core パッケージの一部です。このパッケージは、フォント スタックを指定するのと同じ順序でフォント指標の配列(font-family プロパティ)を受け取ります。

Capsize の使用については、こちらのドキュメントをご覧ください。

次の例で考えてみましょう。対象のウェブフォントは Lobster で、Helvetica Neue、snippet にフォールバックしています。CSS では、font-family: Lobster, 'Helvetica Neue', Arial

  1. コアパッケージから createFontStack をインポートします。

    import { createFontStack } from '@capsizecss/core';
    
  2. 目的の各フォントのフォント指標をインポートします(上記のフォント指標をご覧ください)。 javascript import lobster from '@capsizecss/metrics/lobster'; import helveticaNeue from '@capsizecss/metrics/helveticaNeue'; import arial from '@capsizecss/metrics/arial';`

  3. font-family CSS プロパティを使用する場合と同じ順序で、指標を配列として渡し、フォント スタックを作成します。 javascript const { fontFamily, fontFaces } = createFontStack([ lobster, helveticaNeue, arial, ]);

これにより、次の結果が返されます。

{
  fontFamily: Lobster, 'Lobster Fallback: Helvetica Neue', 'Lobster Fallback: Arial',
  fontFaces: [
    {
      '@font-face' {
      'font-family': '"Lobster Fallback: Helvetica Neue"';
      src: local('Helvetica Neue');
      'ascent-override': '115.1741%';
      'descent-override': '28.7935%';
      'size-adjust': '86.8251%';
      }
     '@font-face' {
       'font-family': '"Lobster Fallback: Arial"';
       src: local('Arial');
       'ascent-override': 113.5679%;
       'descent-override': 28.392%;
       'size-adjust': 88.053%;
     }
   }
 ]
}

fontFamily と fontFaces のコードを CSS に追加する必要があります。次のコードは、CSS スタイルシートまたは <style> ブロック内に実装する方法を示しています。

<style type="text/css">
  .heading {
    font-family: 
  }

  
</style>

これにより、次の CSS が生成されます。

.heading {
  font-family: Lobster, 'Lobster Fallback: Helvetica Neue',
    'Lobster Fallback: Arial';
}

@font-face {
  font-family: 'Lobster Fallback: Helvetica Neue';
  src: local('Helvetica Neue');
  ascent-override: 115.1741%;
  descent-override: 28.7935%;
  size-adjust: 86.8251%;
}
@font-face {
  font-family: 'Lobster Fallback: Arial';
  src: local('Arial');
  ascent-override: 113.5679%;
  descent-override: 28.392%;
  size-adjust: 88.053%;
}

また、@capsize/metrics パッケージを使用してオーバーライド値を計算し、その値を CSS に適用することもできます。

const fontMetrics = require(`@capsizecss/metrics/inter`);
const fallbackFontMetrics = require(`@capsizecss/metrics/arial`);
const mainFontAvgWidth = fontMetrics.xAvgWidth / fontMetrics.unitsPerEm;
const fallbackFontAvgWidth = fallbackFontMetrics.xAvgWidth / fallbackFontMetrics.unitsPerEm;
let sizeAdjust = mainFontAvgWidth / fallbackFontAvgWidth;
let ascent = fontMetrics.ascent / (unitsPerEm * fontMetrics.sizeAdjust));
let descent = fontMetrics.descent / (unitsPerEm * fontMetrics.sizeAdjust));
let lineGap = fontMetrics.lineGap / (unitsPerEm * fontMetrics.sizeAdjust));

謝辞

Alexander Andrews 氏による Unsplash のヒーロー画像