⌨️

Next13新機能、@next/fontでフォント読み込みを高速化してみた

2022/11/08に公開

https://nextjs.org/docs/pages/building-your-application/optimizing/fonts

この記事でやること

  • @next/fontの導入
  • @next/font導入前後でフォントファイルのリードタイムを比較

以上2点です(PagesRouter環境)
ちなみに@next/fontはGoogleFontsとローカルフォントの2系統に対して使うことができるのですが、今回は両方試してみたいと思います。

実行環境

  • bun 1.0.2
  • node: v20.6.1
  • next: 13.5.1
  • react: 18.2.0
  • react-dom: 18.2.0

next/fontの概要

まずは@next/fontの概要についてざっくりと。以下、Next公式Docsからの引用です(Google翻訳)

@next/font は、フォント (カスタム フォントを含む) を自動的に最適化し、外部ネットワーク リクエストを削除して、プライバシーとパフォーマンスを向上させます。

@next/font には、任意のフォント ファイル用の組み込みの自動セルフ ホスティングが含まれています。 これは、基礎となる CSS の size-adjust プロパティが使用されているおかげで、レイアウト シフトなしで Web フォントを最適にロードできることを意味します。

この新しいフォント システムでは、パフォーマンスとプライバシーを考慮して、すべての Google フォントを便利に使用することもできます。 CSS とフォント ファイルはビルド時にダウンロードされ、残りの静的アセットと共に自己ホストされます。 ブラウザから Google にリクエストが送信されることはありません。

GoogleFontsをセルフホスティングしてNext側で最適化する、ということでしょうか。

next/font導入

next v13.0.0の頃は@next/fontパッケージのインストールが必要でしたが、現在は必要ないようです(v13.5.1)
create-next-app後、すぐにnext/fontを使用することができます。

GoogleFontsのnext/font対応

まずはGoogleFontsをnext/fontに対応させていきます。

元々は_document.tsx内の<Head>タグ内でGoogleFontsを読み込んでいましたが、必要ないためこれを削除します。

pages/_document.tsx
import Document, { Html, Head, Main, NextScript } from "next/document";
import React from "react";

const document: React.FC<Document> = () => {
  return (
    <Html className="light">
      <Head>
-       <link
-        href="https://fonts.googleapis.com/css2?-- family=Zen+Kaku+Gothic+Antique:wght@400;700&display=swap"
-          rel="stylesheet"
-       />
      </Head>
      <body>
        <Main />
        <NextScript />
      </body>
    </Html>
  );
};

export default document;

そして、_app.tsx内で以下のような記述を行いました。

pages/_app.tsx
//~~省略~~

+ import { Zen_Kaku_Gothic_Antique } from "@next/font/google";

+ const ZenKakuGothicNew = Zen_Kaku_Gothic_Antique({
+  display: 'swap',
+  weight: ['400', '700'],
+  preload: false,
+ })
+ const SourceCodePro = Source_Code_Pro({
+  subsets:['latin'],
+ display: 'swap',
+  weight: ['400', '700'],
+ })

const MyApp = ({ Component, pageProps }: AppPropsWithLayout) => {
  const getLayout = Component.getLayout ?? ((page) => page);
  return getLayout(
    <>
+     <style jsx global>{`
+        html {
+         font-family: ${ZenKakuGothicAntique_normal.style.fontFamily},
+           ${ZenKakuGothicAntique_bold};
+       }
+     `}</style>
      <Component {...pageProps} />
    </>
  );
};

export default MyApp;

やっていること

  • GoogleFontsの使用したいフォント名を@next/font/googleから分割代入でインポートし、constで格納。このとき、weightsubsetプロパティを指定しているのがポイント。

  • weight:使いたいfont-weightを指定。variable fontsでないフォントファミリーは、必ずこのweightを指定する必要があります。ちなみに、複数の太さを['400', '700']のように、使用したいfont-weightを複数指定することも可能です。

  • subsets: 複数の言語に対応しているフォントファミリー(Robotoなど)の場合、どの言語でサブセット化するかを指定します。日本語フォントのようにsubsetsが必要ない場合はpreload: falseを追記することでsubsetsプロパティをキャンセルできるようです。

ローカルフォントの読み込み

googlefontsと同じく_app.tsx内にローカルフォント読み込みの記述を追加

pages/_app.tsx
//~~省略~~

  import { Zen_Kaku_Gothic_Antique } from "@next/font/google";
+ import localfont from "@next/font/local";

  const ZenKakuGothicAntique_normal = Zen_Kaku_Gothic_Antique({
   weight: "400",
   subsets: ["japanese"],
  });
  const ZenKakuGothicAntique_bold = Zen_Kaku_Gothic_Antique({
   weight: "700",
   subsets: ["japanese"],
  });
  
  //pages > KikaiChokokuJISMd.woff
+ const kiChoJIS = localfont({
+  src: "./KikaiChokokuJISMd.woff",
+  variable: "--kicho-jis",
+ });

  const MyApp = ({ Component, pageProps }: AppPropsWithLayout) => {
   const getLayout = Component.getLayout ?? ((page) => page);
   return getLayout(
    <>
     <style jsx global>{`
        html {
         font-family: ${ZenKakuGothicAntique_normal.style.fontFamily},
           ${ZenKakuGothicAntique_bold},
+	   ${kiChoJIS.variable}
	   ;
       }
     `}</style>
      <Component {...pageProps} />
    </>
  );
};

export default MyApp;

これで実装は終わりです。が、ローカルフォントの導入では2点ハマりポイントがありました。

ハマりポイント1:ローカルフォントファイルの置き場所

画像・フォントなどのアセットはpublic/にまとめる慣習があるかと思いますが、@next/fontを使う場合、ローカルフォントのファイルはpagesディレクトリ内でなければならないようです(多分)
public/fonts/ファイルのような配置ではエラーが発生、buildが失敗しました。

ビルド時に遭遇したエラー
Failed to compile.

Font loader error:
Can't resolve './fonts/KikaiChokokuJISMd.woff' in 'C:\Users\~~~'

Location: pages\_app.tsx

ハマりポイント2:ローカルフォントが反映されない

当初はGoogleFontsと同様の記述で読み込んでいましたが、これだとフォントが反映されませんでした。
これについては、variableプロパティを指定し、フォントのcss変数を定義することでクリアできました。(詳しくは公式Docsのここを)

_app.tsx
//上記完成コードより抜粋。
+ const kiChoJIS = localfont({
+  src: "./KikaiChokokuJISMd.woff",
+  variable: "--kicho-jis", //css変数名を定義する必要がある。
+ });
//~中略~
+${kiChoJIS.variable} //MyApp関数内での呼び出しはこうなる
//~中略~

あとは使いたいCSSファイルで定義した変数名を指定してあげればOK。

.sometext {
  font-family: var(--kicho-jis);
}

フォントファイルのリードタイムはどう変わる?

Chrome Devtoolのnetworkから、next/font導入前/後のリードタイムを比較してみたいと思います。

next/font導入前


このような形で、GoogleFontsのリクエストがたくさんあります。(リクエストが多すぎて画像に収まっていないです)

next/font導入後


next/font導入後は、GoogleFontsのリクエストはゼロに!Nextの謳い文句どおりですね。
しかし、ローカルフォントのリクエストは発生しています。
とは言えローカルフォントのリードタイムも導入前291ms -> 導入後243ms50ms程度低くなっていたため、ある程度の効果はある?のではないかと思います(何度か計測しましたが、多少低くなる傾向にあります)

AppRouterでもやってみた(2023/09/21追記)

この記事を公開したのは去年の11月ということもあり、PagesRouter環境での導入方法を紹介しています。
しかしながらv13.4からAppRouterがstableになったということで、今後はあまり価値のない情報になっていくかと思われます。(そもそもNextJSの公式Docs以上に分かりやすい情報は無い)

AppRouterでnext/fontsを導入したリポジトリとサンプルページを公開していますので、大した内容ではありませんがよければご覧ください。サンプルページではGoogleフォントの日本語と英語を使用しており、実際のパフォーマンスを計測することも可能です。

Discussion