日本語フォントを入れたら遅くなったのでやめた
Ryoma
メンバー
お疲れ様です。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だけでも重い
- 削ったら測る。数字が出ると、判断に自信が持てる
それではまた!
