⌨️

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

2022/11/07に公開約6,200字

先日発表されたNext13で盛り込まれた新機能、@next/fontsを試してみたので、備忘録も兼ねて記事にしてみました。

https://nextjs.org/docs/basic-features/font-optimization

この記事でやること

  • @next/fontを開発中のプロジェクトへ導入+ハマった箇所解説
  • @next/font導入前後でフォントファイルのリードタイムを比較

以上2点です。
現在開発中の個人ポートフォリオ兼技術ブログを実験台にします。
ちなみに@next/fontはGoogleFontsとローカルフォントの2系統に対して使うことができるのですが、今回は両方試してみたいと思います。

実行環境

  • node: v18.12.0
  • next: 13.0.0
  • react: 18.2.0
  • react-dom: 18.2.0
  • TypeScript
  • hosting: vercel hobbyプラン
  • OS: Windows10

next/fontの概要

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

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

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

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

GoogleFontsをセルフホスティングしてNext側で最適化する、というのは中々なパワープレイですね~。

next/font導入

公式Docs通りに作業を進めています。
ということでまずは、 @next/fontのnpmパッケージをインストール。

npm install @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 ZenKakuGothicAntique_normal = Zen_Kaku_Gothic_Antique({
+   weight: "400",
+   subsets: ["japanese"],
+ });
+ const ZenKakuGothicAntique_bold = Zen_Kaku_Gothic_Antique({
+   weight: "700",
+   subsets: ["japanese"],
+ });

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']みたいに一括指定したい所ですが無理でした。なので使いたい太さごとに変数定義してます。

  • subsets: 複数の書体を持つフォントファミリーの場合、どの書体をサブセット化するかを指定します(これなしだとビルド時にwarn出ました。書体が1つしか無いフォントだったら関係無さそう。)

ローカルフォントも同じ要領で@next/font化する

ローカルフォントは元々vanilla-extractのglobalFontFaceを使って読み込んでいますが、これも削除します。

text.css.ts
//~~省略~~

- globalFontFace("kiChoJIS", {
-  fontDisplay: "swap",
-  src: "url(/fonts/KikaiChokokuJISMd.woff) format('woff')",
- });

そして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程度低くなっていたため、ある程度の効果はある?のではないかと思います(何度か計測しましたが、多少低くなる傾向にあります)

終わり

next/fontの導入方法と、パフォーマンス面での変化について駆け足ですが書いてみました。

漢字/ひらがな/カタカナとたくさんの文字種がある日本語Webフォントというのは、Webサイト実装で取り扱うアセットの中でも特級に重いので、フォントオタクの自分としてはnext/fontの登場は個人的にかなりありがたいです。
(最近Webサイト制作はAstroでよくね??Nextはハイスペすぎるぜ、、、と思っていた矢先なのですが、こういう機能を見せつけられるとやはり心が揺れ動きますw)

解釈のおかしい発言や記述ミスの1つや2つや3つはあると思いますので、もし見つけたらコメントなんかで教えていただけると嬉しいです!

お読みいただきありがとうございました!

Discussion

ログインするとコメントできます