🐢

Next.js のWebfont自動最適化によってパフォーマンスが劣化していた件

2023/10/05に公開

TL;DR

  • Next.jsで構築されたWebサイトが、特定の状況下でパフォーマンス問題が発生していました。
  • 問題の原因は、Automatic Webfont Optimization機能によってHTMLサイズが著しく膨張していたことでした。
  • 解決策として、next/fontを利用し、Webfontの読み込み方法を最適化し、HTMLサイズを削減しました。

プロジェクトの固有の事情

  • Next.js アプリケーションを ECS にデプロイしてテスト運用している
  • 多言語対応(日本語,韓国語,中国語(繁体字,簡体字))している
  • もともと Next.js v12 で作り始め、先月やっと v13 にアップデートが完了した

発生していた問題

私達のチームが開発しているNext.jsで構築された特定のWebサイトにおいて、パフォーマンス計測を行う機会がありました。そこで、意外なことに、リクエスト数がそれほど多くないにも関わらず、サーバ側でIPC(プロセス間通信)関連のエラーが発生していることに気づきました。

(発生していたエラーの一部)
TypeError: fetch failed
    at Object.fetch (node:internal/deps/undici/undici:11457:11)
    at async invokeRequest (/home/snaka/xxxxx/node_modules/next/dist/server/lib/server-ipc/invoke-request.js:17:12)
    at async invokeRender (/home/snaka/xxxxx/node_modules/next/dist/server/lib/router-server.js:254:29)
    at async handleRequest (/home/snaka/xxxxx/node_modules/next/dist/server/lib/router-server.js:447:24)
    at async requestHandler (/home/snaka/xxxxx/node_modules/next/dist/server/lib/router-server.js:464:13)
    at async Server.<anonymous> (/home/snaka/xxxxx/node_modules/next/dist/server/lib/start-server.js:117:13) {
  cause: Error: read ECONNRESET
      at TCP.onStreamRead (node:internal/stream_base_commons:217:20) {
    errno: -104,
    code: 'ECONNRESET',
    syscall: 'read'
  }
}

上記とは別のエラーも

cause: SocketError: other side closed
at Socket.onSocketEnd (/app/node_modules/next/dist/compiled/undici/index.js:1:63301)
at Socket.emit (node:events:525:35)
at endReadableNT (node:internal/streams/readable:1359:12)
at process.processTicksAndRejections (node:internal/process/task_queues:82:21) {
  code: 'UND_ERR_SOCKET',
  socket: {
    localAddress: '127.0.0.1',
    localPort: 45850,
    remoteAddress: undefined,
    remotePort: undefined,
    remoteFamily: undefined,
    timeout: undefined,
    bytesWritten: 2937,
    bytesRead: 87753
  }
}

さらに調査を進めると、Next.jsでビルドされたHTMLが異常に大きいことが明らかになりました。具体的には、そのサイズは約2MBで、一般的なウェブページのHTMLサイズと比較しても著しく大きなものでした。

問題の原因

私たちのWebサイトで発生していたパフォーマンスの問題を解決するため、まずはその原因を突き止めるべく、一連の調査を開始しました。負荷試験を実施中に、ネットワーク帯域が明らかなボトルネックとなっていることが判明し、特に単一のHTMLファイルのダウンロードで著しく帯域を消費している点が気になりました。

さらにHTMLの調査を進めると、大量のフォント定義(@font-faceルール)の記述が含まれていることが発覚しました。これらの記述は元々のソースコードには存在せず、何らかの仕組みによって自動的に挿入されているものでした。さらなる調査により、これらのフォント定義は Next.js v10.2 で導入された Automatic Webfont Optimization 機能に由来するものであることが判明しました。

参考: Next.js 10.2 のリリースノート

問題となっていた箇所は _document.tsx の以下の記述でした。

// _document.tsx
<Head>
  <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Noto+Sans+JP:wght@100;300;400;500;700;900&display=swap" />
  <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@100;300;400;500;700;900&display=swap" />
  <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Noto+Sans+TC:wght@100;300;400;500;700;900&display=swap" />
  <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Noto+Sans+KR:wght@100;300;400;500;700;900&display=swap" />
</Head>

サンプルコードは以下で公開しています

https://github.com/snaka/code-pocket/tree/master/framework/nextjs/webfont/legacy-optimization

ビルド結果のHTMLのサイズ (約2MB)
-rw-r--r--@ 1 snaka  staff  2359534 Sep 29 09:48 ja.html
-rw-r--r--@ 1 snaka  staff  2359533 Sep 29 09:48 ko.html
-rw-r--r--@ 1 snaka  staff  2359540 Sep 29 09:48 zh-CN.html
-rw-r--r--@ 1 snaka  staff  2359540 Sep 29 09:48 zh-TW.html

解決方法

解決方法は簡単でした。 _document.tsx でのWebfontの読み込みをやめて Next.js v13から導入されたnext/fontを利用する方法に切り替えました。

https://nextjs.org/docs/pages/building-your-application/optimizing/fonts

この変更により、HTMLのサイズが大幅に削減され、サーバに負荷をかけた際のIPCエラーも解消されました。さらに、next/fontの利用により、Webfontをセルフホストすることが可能となり、CDNを通じてキャッシュ期間などを独自にコントロールすることもできるようになりました。

Webfont を利用するコードは以下のようになりました。

// index.tsx
import { Noto_Sans_JP, Noto_Sans_KR, Noto_Sans_SC, Noto_Sans_TC } from 'next/font/google'

const notoSansJP = Noto_Sans_JP({
  weight: ['100', '300', '400', '500', '700', '900'],
  preload: false
})
const notoSansSC = Noto_Sans_SC({
  weight: ['100', '300', '400', '500', '700', '900'],
  preload: false
})
const notoSansTC = Noto_Sans_TC({
  weight: ['100', '300', '400', '500', '700', '900'],
  preload: false
})
const notoSansKR = Noto_Sans_KR({
  weight: ['100', '300', '400', '500', '700', '900'],
  preload: false
})
// 上記で読み込んだ webfont を className={notoSans_JP.className} のように利用する

サンプルコードは以下で公開しています。

https://github.com/snaka/code-pocket/tree/master/framework/nextjs/webfont/next-font

ビルド結果のHTMLサイズ (約5KB)
-rw-r--r--@ 1 snaka  staff   5176 Sep 29 14:19 ja.html
-rw-r--r--@ 1 snaka  staff   5175 Sep 29 14:19 ko.html
-rw-r--r--@ 1 snaka  staff   5182 Sep 29 14:19 zh-CN.html
-rw-r--r--@ 1 snaka  staff   5182 Sep 29 14:19 zh-TW.html

対応の結果として、Lighthouse の Performance の数値は 52 だったものが、59 とわずかですが改善しました。( まだまだ改善の余地ありです )

対応方法の選択にあたって

今回のケースでは Webfont の読み込み方法を変更し next/font を preload なしで利用するという方法を採用しましたが、この選択は以下のような技術的なトレードオフを評価した結果の判断です。

  • 改善前の課題
    • 通信量の増加によりコンテナ内でのプロセス間通信が不安定となっていた
    • コンテナに割り当てたCPUやメモリ性能が十分に活用できず、多くのコンテナを並行稼働させる必要があった
  • next/fontに変更することによるネガティブな影響
    • next/fontをpreloadなしで利用 → Webfont適用までのわずかなタイムラグが発生した
  • 代替案の検討
    • デプロイ先をFaaS基盤に変更する?→サービス稼働までの技術検証する時間が確保できないため、高リスクと判断
    • 個人的にはとても興味があったが...
  • 最終的な選択
    • next/fontを利用し通信のボトルネックを解消とコンテナリソースの有効活用のメリットを得る
    • コストパフォーマンスとわずかなUXのデグレを比較し、コストパフォーマンスを優先した
    • ファイル読み込みを分割した結果、CDNの利用でサーバ負荷を分散させ、コスト効率よくサービスの安定性を実現することが可能となると見込んだ

この選択によって、読み込むべきファイルが増えたことでクライアント側のオーバーヘッドは増えましたが、最終的なパフォーマンスの評価(Lighthouse)としては多少向上した結果となりました。(約13%改善)

さいごに

この記事では、Next.jsで構築されたWebサイトにおいて発生したパフォーマンス問題とその解決策についてひとつの例を示しました。Automatic Webfont Optimization機能が、特定のケースへHTMLサイズの著しい膨張を引き起こし、結果としてサーバ側でIPCエラーを誘発する可能性があることを確認しました。

解決策として、next/fontを利用し、Webfontの読み込み方法を最適化することでHTMLサイズを削減し、サーバのボトルネックを解消することができました。これによって、Webfontをセルフホストし、キャッシュのコントロールも可能となりました。

フロントエンド技術の進化のスピードは凄まじい勢いで、日々新しい技術や最適化手法が提供されています。それらを適切に利用することでWebサイトのパフォーマンスを向上することができますが、それらも万能ではなく、場合によっては予期せぬ問題を引き起こすことがあります。そのため、新しい機能を導入する際には十分なテストが欠かせません。

この記事が誰かの役に立つことがあれば幸いです。

※この記事はChatGPTに校正してもらいました。

参考としたページ

https://nextjs.org/docs/pages/building-your-application/optimizing/fonts

https://nextjs.org/blog/next-10-2#automatic-webfont-optimization

https://zenn.dev/catnose99/articles/bb943c3dc99d89

Discussion