yuheijotaki.com

Astro Fonts API を試してみる

目次

Astro Fonts API とは

astro.config.mjsfonts 配列でフォントを宣言し、<Font /> コンポーネントを <head> に置くと @font-face と preload link が出力される。

取得したフォントはローカル化されて、fallback フォントもメトリクス込みで自動生成される。組み込みプロバイダーは adobe / bunny / fontshare / fontsource / google / googleicons / local / npm の 8 種。

今回は Google プロバイダーで Noto Sans JP を読み込んで、生成される <head> と日本語フォントのサブセットがどう扱われるかを見ていく。動作確認は astro@6.3.3

設定

import { defineConfig, fontProviders } from 'astro/config';

export default defineConfig({
  fonts: [
    {
      provider: fontProviders.google(),
      name: 'Noto Sans JP',
      cssVariable: '--font-noto-sans-jp',
      weights: [400, 700],
      subsets: ['japanese'],
    },
  ],
});

<head> 側:

---
import { Font } from 'astro:assets';
---

<Font cssVariable="--font-noto-sans-jp" preload />

生成された出力

npm run build 後の dist/client/_astro/fonts/ を見ると、woff2 が 120 ファイル / 合計 5.2 MBweights 2 種 × unicode-range で 60 分割されている計算で各ファイルは 12〜88KB。

<head> には unicode-range 付きの @font-face が並ぶ。

<style>
  @font-face {
    font-family: 'Noto Sans JP-86c17494f12e7c6c';
    src: url('/_astro/fonts/5bcf9a2144f25cf1.woff2') format('woff2');
    font-display: swap;
    unicode-range: U+25ee8, U+25f23, U+25f5c, U+25fd4, /* ... 多数 ... */;
    font-weight: 400;
    font-style: normal;
  }
  /* ...同じ要領で 120 個続く... */
</style>

font-family 名にハッシュのサフィックスが付いて、同じファミリー名が複数の <Font /> 設定で衝突しないようになっている。

fallback も自動で出力される。

@font-face {
  font-family: 'Noto Sans JP-86c17494f12e7c6c fallback: Arial';
  src: local('Arial');
  font-display: swap;
  size-adjust: 197.1733%;
  ascent-override: 58.8315%;
  descent-override: 14.6064%;
  line-gap-override: 0%;
}

src: local('Arial') でローカルにある Arial を使い、size-adjustascent-override 等でメトリクスを Noto Sans JP に合わせて、ロード前後の見た目のズレを抑えてくれる。

preload link は <Font preload /> のままだと 120 個ぜんぶ<link rel="preload"> が並ぶ。日本語のように分割数が多いフォントではここを絞らないと過剰。

内部の挙動

preload プロパティは boolean だけでなく filter を渡せる。filter-preloads.ts を読むと、weight / style / subset で絞り込めるのが分かる。

コードを開閉する
export function filterPreloads(
  data: Array<PreloadData>,
  preload: PreloadFilter,
): Array<PreloadData> | null {
  if (!preload) {
    return null;
  }
  if (preload === true) {
    return data;
  }
  return data.filter(({ weight, style, subset }) =>
    preload.some((p) => {
      if (
        p.weight !== undefined &&
        weight !== undefined &&
        !checkWeight(p.weight.toString(), weight)
      ) {
        return false;
      }
      if (p.style !== undefined && p.style !== style) {
        return false;
      }
      if (p.subset !== undefined && p.subset !== subset) {
        return false;
      }
      return true;
    }),
  );
}

<Font preload={[{ weight: 400, subset: 'japanese' }]} /> のように書くと、特定の組み合わせだけに preload を絞れる。日本語フォントでは全 chunk を preload するのは過剰なので、こうした filter で絞る前提になりそう。

日本語フォントとサブセット

Noto Sans JP のフルセットは数 MB あって、何の工夫もなしに読ませると重い。Google プロバイダーで subsets: ['japanese'] を指定した場合、Google Fonts の CSS2 API がそもそも unicode-range で 60 分割した CSS を返してくる。Astro はそれをそのまま読んで、各 chunk の woff2 をローカルへコピーしている。つまり、Astro 自身が分割しているわけではなく、Google 側の分割をそのまま使っているということ。ブラウザは表示する文字に応じて必要な chunk だけ取りに行く動きになる。

所感

同一オリジンから配信してくれる、且つサブセット分割なども処理されるので、要件でリソースはセルフホストが必要な場合などに使えそう。