【Next.js】@next/fontが進化していたので導入してみた
Next.jsのv13で新たに実装された@next/font
が進化していたので、自身のポートフォリオに導入してみました。
この記事では、私の環境(ごく一般的なもの)に合わせた実装手順と、過去にできなかったことと現在できるようになった部分を併せて記載していきます。
📦 はじめに
@next/fontについて
ざっくりですが、GoogleFontとローカルのフォント、両方に対してとにかく最適化を行なってくれます。
フォントファイルとCSSがビルド時にダウンロードされ、静的アセットとして自己ホストされると公式には書いてありました。
詳しくは下記からご覧ください。
ポートフォリオの環境
導入した私のポートフォリオサイトの環境です。
"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の読み込みを行なっていました。
以下のような感じです。
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されている部分が今回のフォント指定に関する部分です。
実際のソース
+ 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が見つかったらそれに置き換えて再描画しますよ、という設定です。
時系列でいうと...
- ブラウザからアクセスする
- 代替テキストで描画される(まだWebFontがダウンロードされていない)
- WebFontがダウンロードされる
- 指定された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を組み合わせているようなサイトでは如実な数値が出るかもしれません。
過去に試していたときは、ほんとに指定したフォントの太さごとの数しかリクエストしていなかった気がするんですが、なんか変わったのか僕の実装に問題があるのか...もう少し勉強します。
📚 参考記事
たいへん参考になりました。ありがとうございました。
🦄 おわりに
まだ理解しきっていないのですが、いったんNext.js(v.13)の機能でリファクタできてよかったです。
今年もよろしくお願いいたします🐰🦄
Discussion