🦄

【Next.js】@next/fontが進化していたので導入してみた

2023/01/01に公開

Next.jsのv13で新たに実装された@next/fontが進化していたので、自身のポートフォリオに導入してみました。

この記事では、私の環境(ごく一般的なもの)に合わせた実装手順と、過去にできなかったことと現在できるようになった部分を併せて記載していきます。

📦 はじめに

@next/fontについて

ざっくりですが、GoogleFontとローカルのフォント、両方に対してとにかく最適化を行なってくれます。

フォントファイルとCSSがビルド時にダウンロードされ、静的アセットとして自己ホストされると公式には書いてありました。

詳しくは下記からご覧ください。

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

ポートフォリオの環境

導入した私のポートフォリオサイトの環境です。

"dependencies": {
    "@apollo/client": "^3.7.1",
    "@emotion/react": "^11.10.5",
    "@emotion/styled": "^11.10.5",
    "@next/font": "^13.0.3",
    "@octokit/core": "^4.1.0",
    "dayjs": "^1.11.6",
    "graphql": "^16.6.0",
    "next": "13.0.2",
    "react": "18.2.0",
    "react-dom": "18.2.0"
  },
  "devDependencies": {
    "@types/node": "18.11.9",
    "@types/react": "18.0.25",
    "@types/react-dom": "18.0.8",
    "eslint": "8.27.0",
    "eslint-config-next": "13.0.2",
    "husky": "^8.0.0",
    "typescript": "4.8.4"
  },
  "volta": {
    "node": "16.18.1"
  }

(箇条書きで書くのが面倒だったのでpackage.jsonから切り取りました)

🗒 導入手順

1. @next/fontをインストール

Next.jsのアプリケーション直下で以下を実行します。

npm i @next/font

2. 不要ファイルを削除

これまでは_document.tsxを作成し、そこでGoogleFontの読み込みを行なっていました。

以下のような感じです。

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

export default function Document() {
  return (
    <Html>
      <Head>
        {/* WebFontの読み込み */}
        <link rel="preconnect" href="https://fonts.googleapis.com" />
        <link rel="preconnect" href="https://fonts.gstatic.com" />
        <link
          href="https://fonts.googleapis.com/css2?family=M+PLUS+1p:wght@400;700;900&display=swap"
          rel="stylesheet"
        />
      </Head>
      <body>
        <Main />
        <NextScript />
      </body>
    </Html>
  );
}

<link>が不要になるかと思いきや、このファイルではフォントの読み込みしか行なっていなかったためファイルごと削除しました。

_document.tsxさん、今までありがとうございました。

3. _app.tsxでフォントの読み込み

次に、新しくインストールしておいた@next/fontを使い、今度は_app.tsx内でフォントの指定を行います。

こんな感じの設計と実装してますということをお伝えするため、あえて_app.tsx全量を記載してみました。

addされている部分が今回のフォント指定に関する部分です。

実際のソース

_app.tsx
+ import { M_PLUS_1 } from "@next/font/google"; // importする
import "@/styles/globals.css";
import type { AppProps } from "next/app";
import { ThemeProvider } from "@emotion/react";
import { ThemeContext } from "@/lib/store/theme";
import { theme, nightTheme } from "@/theme/theme";
import { useState } from "react";
import { Head } from "@/components/organisms/Head/Head";
import { Menu } from "@/components/organisms/Menu/Menu";
import { NormalTemp } from "@/components/templetes/NormalTemp/NormalTemp";
import { Footer } from "@/components/molecules/Footer/Footer";
import styled from "@emotion/styled";

+ const mPlus1 = M_PLUS_1({
+   weight: ["400", "700", "900"], // 使用する太さを配列で指定
+   style: ["normal"],
+   subsets: ["japanese"],
+   display: "swap", // ここが大事
+ });

+ const GlobalFontFamilyStyled = styled.div({
+   fontFamily: mPlus1.style.fontFamily,
+ });

function MyApp({ Component, pageProps }: AppProps) {
  // NightModeに切り替えるステート
  const [isNightMode, setIsNightMode] = useState<boolean>(false);

  return (
    <>
      <ThemeProvider theme={isNightMode ? nightTheme : theme}>
        <ThemeContext.Provider value={{ isNightMode, setIsNightMode }}>
          <Head>
            <meta charSet="utf-8" />
            <meta
              name="viewport"
              content="initial-scale=1.0, width=device-width"
            />
            <meta
              property="description"
              content="やっくんのポートフォリオです。Webデザインとかフロントエンドとかやったりやってなかったりします。"
            />
            <meta property="og:title" content="Yakkun Lab" />
            <meta
              property="og:description"
              content="やっくんのポートフォリオです。Webデザインとかフロントエンドとかやったりやってなかったりします。"
            />
            <meta
              property="og:image"
              content={`https://pk-yakkun.com/images/og/ogp_l_yakkun-lab.png`}
            />
            <meta name="twitter:card" content="summary_large_image" />
            <meta name="twitter:title" content="Yakkun Lab" />
            <meta
              name="twitter:description"
              content="やっくんのポートフォリオです。"
            />
            <meta
              name="twitter:image"
              content="https://pk-yakkun.com/images/og/ogp_l_yakkun-lab.png"
            />
          </Head>
+          <GlobalFontFamilyStyled>
            <NormalTemp>
              <Component {...pageProps} />
            </NormalTemp>
+          </GlobalFontFamilyStyled>
          <Footer />
          <Menu />
        </ThemeContext.Provider>
      </ThemeProvider>
    </>
  );
}

export default MyApp;

まず@next/font/googleから、私がこれまで利用していたM+のフォントをimportします。

ローカルフォントを使う場合は@next/font/localになります。

+ import { M_PLUS_1 } from "@next/font/google"; // importする

フォント情報の指定

定数に使用するフォントの情報を持たせます。

+ const mPlus1 = M_PLUS_1({
+   weight: ["400", "700", "900"], // 使用する太さを配列で指定
+   style: ["normal"],
+   subsets: ["japanese"],
+   display: "swap", // ここが大事
+ });

先ほどimportしたM_PLUS_1に利用したい情報を渡し、定数mPlus1に代入します。

ここでちょっと脱線しますが、以前までこのweightを配列で渡すことができなかったと思います(配列で書いてもすべて400の太さになっていた)。

そのためこの場合だと400, 700, 900のweightをもったそれぞれの定数が必要だったのですが...

display: "swap"をつけることで解決しました。

display: "swap"はなんぞやと言いますと、指定したWebFontが見つかったらそれに置き換えて再描画しますよ、という設定です。

時系列でいうと...

  1. ブラウザからアクセスする
  2. 代替テキストで描画される(まだWebFontがダウンロードされていない)
  3. WebFontがダウンロードされる
  4. 指定されたWebFontを使い、再描画 ←Swap!

こんな感じです。

おそらくですが、このSwapがないと配列で情報を渡した際に先頭の数値、ここでいうと400だけが適応され、それ以降を取得しても再描画されなかったのではないでしょうか。

ChromeのdeveloperツールのNetworkタブを見ていた限り、700, 900のフォントデータも返ってきてるのになんで適応されないんや...と思っていた記憶があります。

ともかくこれで異なる太さのフォントも指定することができました。

スタイリング

私はemotionを利用してstyled-componentsのようなスタイリングをしています。

なので、今回もその手法に合わせてみました。

+ const GlobalFontFamilyStyled = styled.div({
+   fontFamily: mPlus1.style.fontFamily,
+ });
+ <GlobalFontFamilyStyled>
   <NormalTemp>
     <Component {...pageProps} />
    </NormalTemp>
+ </GlobalFontFamilyStyled>

<GlobalFontFamilyStyled>でWrapした部分に適応されます。

<Footer />は文字が小さいためM+だと違和感があったのと、<Menu />にはテキストがないため、あえて対象外にしています。

ただこの実装であるがゆえに、描画時にスタイルがあたる=レイアウトシフトは起きてしまうので、フォント情報をビルド時に保持する恩恵は最大限に得られていないのかなとか思っています。

⏳ 改善結果

同じ本番環境で計測した反映前、後のFontリクエスト部分です。

どちらもChromeのシークレットブラウザです。

before

after

たしかにリクエスト数は減っていますが...あんまり変わってないですね。

もっと多くのWebFontを組み合わせているようなサイトでは如実な数値が出るかもしれません。

過去に試していたときは、ほんとに指定したフォントの太さごとの数しかリクエストしていなかった気がするんですが、なんか変わったのか僕の実装に問題があるのか...もう少し勉強します。

📚 参考記事

たいへん参考になりました。ありがとうございました。

https://zenn.dev/siino/articles/b42d658af571f0

🦄 おわりに

まだ理解しきっていないのですが、いったんNext.js(v.13)の機能でリファクタできてよかったです。

今年もよろしくお願いいたします🐰🦄

Discussion