⚡

Astro の静的サイトで JS・CSS・画像・フォントを必要な分だけにして軽くする

公開日約1時間前2026-10-31
更新日約1時間前2026-10-31
MarkdownRaw Content

この記事のポイント

  • 小さな React のアイランドを .astro + <script> に書き直すと、記事のページの JS が 119KB → 5KB(gzip 後)になった
  • 一部の記事でしか使わない CSS(KaTeX)は、?url で別ファイルにして、使う記事でだけ読み込む
  • 画像は <Picture> で AVIF / WebP に変換し、一覧の画像は遅延読み込みにする
  • フォントは Fonts API で、使う文字の範囲のファイルだけを登録して preload する

小さな React のアイランドをやめる

目次、テーマの切り替え、ナビゲーションのように、状態が少なく HTML の構造が変わらない部品は、React でなくても作れます。<script> の中で Custom Element にすると、同じ部品がページに複数あっても、それぞれ初期化できます。

<theme-switcher>
  <button data-theme="light">Light</button>
  <button data-theme="dark">Dark</button>
</theme-switcher>

<script>
  class ThemeSwitcher extends HTMLElement {
    connectedCallback() {
      for (const button of this.querySelectorAll<HTMLButtonElement>('[data-theme]')) {
        button.addEventListener('click', () => {
          document.documentElement.classList.toggle('dark', button.dataset.theme === 'dark');
        });
      }
    }
  }

  customElements.define('theme-switcher', ThemeSwitcher);
</script>

ドロップダウンは、HTML の popover 属性で作れます。開閉、外側のクリックや Esc キーで閉じる動きは、ブラウザがやってくれます。

<button popovertarget="toc">目次</button>
<div id="toc" popover>...</div>

検索や絞り込みのような大きな部品は、React のままで構いません。本文の埋め込みなど、一部のページでしか使わない React の部品は、要素があるときだけ動的 import します。

if (document.querySelector('[data-embed]')) {
  const { mountEmbeds } = await import('./react-embeds');
  mountEmbeds();
}

ページのスクリプトで react-dom/client を静的に import すると、使わないページでも React が読み込まれるので注意します。

一部の記事でしか使わない CSS を分ける

[slug].astro で import した CSS は、そのルートのすべての記事に入ります。build.inlineStylesheets: 'always' にしていると、記事を開くたびに HTML と一緒に届きます。

数式の CSS のように一部の記事でしか使わないものは、?url で別ファイルにして、必要な記事でだけ <link> で読み込みます。

---
import katexCssUrl from 'katex/dist/katex.min.css?url';

const { Content, remarkPluginFrontmatter } = await render(entry);
---

<Layout>
  {remarkPluginFrontmatter.hasMath && <link slot="head" rel="stylesheet" href={katexCssUrl} />}
  <Content />
</Layout>

<head> に入れるために、レイアウトには <slot name="head" /> を用意しておきます。

数式があるかどうかは、rehype のプラグインで調べて、frontmatter に印を付けます。ここで付けた値は、render() の remarkPluginFrontmatter で受け取れます。

import { EXIT, visit } from 'unist-util-visit';

export default function rehypeMathFlag() {
  return (tree, file) => {
    visit(tree, 'element', (node) => {
      if (!node.properties?.className?.includes?.('katex')) return;
      file.data.astro.frontmatter.hasMath = true;
      return EXIT;
    });
  };
}

このプラグインは、rehype のプラグインの最後に置きます。コードブロックを作り直すプラグインより前に置くと、コードの中の記号を数式と判定してしまうことがあります。

数式のない記事の HTML は、44.3KB → 36.5KB(gzip 後)になりました。

画像を必要な大きさにする

public/ の画像はそのまま配信されます。src/assets/ に置いて <Picture> で表示すると、ビルド時に表示サイズに縮め、AVIF と WebP に変換されます(sharp が必要)。

---
import { Picture } from 'astro:assets';
import avatar from '../assets/avatar.png';
---

<Picture src={avatar} formats={['avif', 'webp']} width={128} height={128} densities={[1, 2]} loading="eager" alt="" />

<Picture> は既定で loading="lazy" なので、ページを開いてすぐ見える画像は eager にします。これで、1024px の PNG(215KB)が 2〜5KB になりました。

一覧に並ぶ画像には、loading="lazy" を付けます。記事一覧で最初に読み込む画像が、108 個から 4 個に減りました。

フォントを Fonts API で読み込む

Fontsource の CSS を import すると、使わない言語の分も含めて、文字の範囲ごとの @font-face がすべて入ります。また、フォントのファイルは、ブラウザが CSS を読んでから読み込み始めます。

Astro の Fonts API で、使う範囲のファイルだけを登録し、<Font> で preload します。fontProviders.local() で npm のパッケージのファイルを指定すれば、ビルドのときに外部に通信しません。

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

export default defineConfig({
  fonts: [
    {
      provider: fontProviders.local(),
      name: 'Inter',
      cssVariable: '--font-inter',
      fallbacks: ['Hiragino Kaku Gothic ProN', 'sans-serif'],
      optimizedFallbacks: false,
      options: {
        variants: [
          {
            src: ['@fontsource-variable/inter/files/inter-latin-wght-normal.woff2'],
            weight: '100 900',
            style: 'normal',
            unicodeRange: ['U+0000-00FF, ...'],
          },
        ],
      },
    },
  ],
});
---
import { Font } from 'astro:assets';
---

<head>
  <Font cssVariable="--font-inter" preload />
</head>

CSS では font-family: var(--font-inter) で参照します。

fontProviders.local() には文字の範囲(subset)の情報がないので、複数のファイルを登録すると、すべてが preload されます。日本語のサイトで英数字にだけ使うフォントなら、latin のファイルだけで足ります。

日本語のサイトでは、optimizedFallbacks を false にしておきます。Astro が作るフォールバック(大きさを合わせた Arial)は文字の範囲を持たないので、→ のような、フォントにない記号まで Arial で表示されてしまいます。

使わない @font-face がなくなり、HTML が約 4KB(gzip 後)小さくなりました。