日本語フォントを入れたら遅くなったのでやめた

日本語フォントを入れたら遅くなったのでやめた

#ウェブサイト #パフォーマンス #Firebase #React
Ryoma

Ryoma

メンバー

9分で読めます

お疲れ様です。TNDメンバーのRyomaです 👋
最近、TNDのX公式アカウントの投稿を分析するWebアプリを作っています。ダッシュボードの見た目をTNDっぽくしたら、表示が2.5秒遅くなり、泣く泣く削った話です😭


作成しているアプリ

Xの投稿の中でどの投稿が伸びるのかを運営メンバーで見るためのアプリです。

  • X APIから、投稿ごとの表示回数やいいねを集める
  • 運営メンバーがログインし、グラフと数字を確認する
  • React + MUI、置き場所はFirebase(Hosting・Authentication・Firestore)
  • 実装はほとんどAI

スマホで手軽に見れるアプリにしたい!そのため、快適に操作する上で大事なのは

開いたらすぐ数字が出ること!

これを忘れないように開発していました。


TNDテーマを使うと、、、

MUIの初期設定のままでは味気ないので、TNDのホームページに寄せて紺 × TNDブルーの配色とロゴを入れました。
書体も2つ入れました。

  • 日本語:M PLUS 1p
  • 数字と見出し:JetBrains Mono

すっきりした日本語に、等幅でそろった数字で正直かなり気に入ってるフォントです。
で、ビルド結果がこちら。

  • CSS:約220KB(gzip後)
  • フォントのファイル:700個以上

AIに軽く実装をお願いするとなんかめっちゃ重くなってました😇

なぜこんなに重くなるのか

日本語は文字種が多く、フォントファイルのサイズが大きくなります。
そこでFontsource(npmでフォントを入れられるパッケージ)は、日本語フォントを文字の範囲ごとに100個以上のファイルに分け、使う文字の分だけ読みます。
仕組みは賢いのですが、「どの範囲がどのファイルか」の定義(@font-face)が太さごとに100個以上CSSに並びます。
3種類の太さで、この定義だけでCSSが膨らんでいました。

結論、日本語フォントをやめた

このアプリに要るのは速く出る数字で、おしゃれな書体ではありません。M PLUS 1pとはさよならすることにしました👋

// fonts.ts(変更前)
import '@fontsource/m-plus-1p/400.css';
import '@fontsource/m-plus-1p/500.css';
import '@fontsource/m-plus-1p/700.css';
import '@fontsource/jetbrains-mono/500.css';
import '@fontsource/jetbrains-mono/700.css';

// fonts.ts(変更後):数字と見出しの書体の、英数字の分だけ
import '@fontsource/jetbrains-mono/latin-500.css';
import '@fontsource/jetbrains-mono/latin-700.css';

日本語は端末の書体(ヒラギノなど)で表示します。
数字はJetBrains Monoのままなので、ダッシュボードの雰囲気はほぼ変わりません。


ついでにJSも半分にした

最初に読むJSは約460KB(gzip後)あったのでこちらもついでに削減しました。行った作業は以下の2点です。

1. FirestoreをLite版にする

Firestoreには、機能を絞ったLite版があります。
リアルタイム更新(onSnapshot)とオフラインのキャッシュが無い代わりに軽くなっています。このアプリでは開いたときに読むだけなので、それで十分だと気づきました。

// 変更前
import { initializeFirestore, persistentLocalCache, persistentMultipleTabManager } from 'firebase/firestore';
const db = initializeFirestore(app, {
  localCache: persistentLocalCache({ tabManager: persistentMultipleTabManager() }),
});

// 変更後
import { getFirestore } from 'firebase/firestore/lite';
const db = getFirestore(app);

データを読み書きする関数は通常版と同じ名前で使えるので、書き換えたのはこのimportと初期化の部分だけです。

2. グラフを後から読む

グラフの部品(MUI X Charts)は約100KB。
最初のJSに入っていると、グラフの部品を読み終わるまで数字すら出ません。
グラフだけ別のファイルに分けて、後から読むようにしました。

// 要点のみの抜粋です
let loading: Promise<typeof import('./charts')> | null = null;
const load = () => (loading ??= import('./charts'));

// ログインを確認できた時点で読み始める(データの読み込みと並行させる)
export const preloadCharts = () => void load();

const Chart = lazy(async () => ({ default: (await load()).MonthlyChart }));

export function MonthlyChart(props: MonthlyChartProps) {
  return (
    // 読み込み中は、グラフと同じ高さの枠を出しておく
    <Suspense fallback={<Skeleton variant="rounded" height={220} />}>
      <Chart {...props} />
    </Suspense>
  );
}

読み込み中の枠をグラフと同じ高さにしておくと、グラフが出た瞬間に画面がずれません。

最初に読み込むJSのサイズは約460KB → 約230KB。きれいに半分です 🎉


実際どれくらい速くなったのか

KBが減っても、速くなっていなければ意味がないので測ってみました。

  • 道具:Lighthouse(Chromeの、ページの速さを測るツール)
  • 設定:モバイル(遅めの4G回線と、処理の遅いスマホを想定)
  • 各時点のコードをビルドし、5回ずつ測った中央値
  • FCP:画面に最初に何かが出るまで
  • LCP:画面のメインの内容が出るまで

日本語フォントでLCPが4.26秒 → 6.75秒。
差は2.5秒です。TND仕様の代償が思ったより高かったです。

フォントをやめて4.70秒。
JSを半分にして、FCPは3.08秒 → 2.03秒。テーマ適用前より速くなりました。

1秒も変わるのは、このアプリがJSを読み終わるまで何も表示しないSPAだからです。
JSはダウンロードのあとに解析と実行があり、遅いスマホほど時間がかかります。
つまり、

最初に読むJSの量が、そのまま真っ白な時間になる

ただし、全部は取り戻せていない

ログイン後のダッシュボード(架空のデータで表示)も測りました。

時点

FCP

LCP

テーマ適用前

2.71秒

3.32秒

日本語フォントを入れた

4.21秒

7.36秒

フォントをやめた

2.78秒

4.66秒

JSを半分にした

1.88秒

4.21秒

最初に何かが出るのは速くなりました。
でもメインの内容は、テーマ適用前のほうがまだ速い。ここまでやって、まだ負けてる……💦
見た目のために足したロゴや書体の分が、まだ残っているようです。次の課題です。

※ 手元のパソコンで、遅い回線とスマホを想定して計算した数字です。実機ではありません。


おわりに

AIに任せて気づいたこと

AIに実装を任せると、想定より重くなります。
頼んだ見た目や機能は、きっちり足してくれます。しかし「そのぶん何秒遅くなるか」は、こちらが聞かないと出てきません。
「TNDっぽく」の結果が、700個のフォントファイルでした。

逆に「軽くして」と頼めば、Lite版への置き換えもグラフの後読みも、すぐ書いてくれました。
重くなるか軽くなるかを決めるのは、AIではなく頼む側です。

実装はAIに任せていい、方針は人が決める

今回の2.5秒で、身をもって学びました。

まとめ

  • 見た目を足すのは一瞬。速さは、気づかないうちに削られる
  • 日本語Webフォントは、CSSだけでも重い
  • 削ったら測る。数字が出ると、判断に自信が持てる

それではまた!

この記事をシェアする