おじんブログ

Webアプリに関する知見とか雑記です

Next.js で SG したサイトがなぜあんなに早いのか、 Chrome DevTools とコードリーディングで調べる

はじめに

静的コンテンツをCDNに乗せておくとアクセス時間が早い、という話ではなく、 Next.js を SG してブラウザで閲覧した時の挙動に絞っています。

TL;DR

  • SGした際に、 getStaticProps() 経由で渡したコンテンツは _next/data/ ディレクトリ配下に JSON 形式として書き出される
  • 該当するローティングに達すると、 JSON を読み込み、クライアントサイドでページを書き換える
  • すべてのコンテンツを一気に読み込むのではなく、ページへのリンクが表示されたら、そのリンク先のコンテンツの JSON を先読みしている
  • つまり、クリックする前に既にコンテンツは読み込まれているので、クライアントサイドの書き換えしか時間がかからない

Chrome DevTools で見てみる

Next.js の公式サイトは SG で構築されています。これを例として見ていきます。 もし初めて見て場合は各リンクをクリックしてみてください。 どのページも一瞬で遷移するのを感じると思います。

https://nextjs.org/

Chrome の DevTools を開き、 Network タブを開き、フィルター欄に json を入力し、一旦更新します。 するとJSONファイルが読み込まれているのがわかります。 f:id:mr_ozin:20211108224946p:plain

Headers タブを開いて Request URL 見ると、サイトのルートの /_next/data/ ディレクトリ配下にこのJSONファイルが格納されているのがわかります。 f:id:mr_ozin:20211108225015p:plain

Preview タブに切り替えてJSONの中身を見るとページの情報が格納されています。 f:id:mr_ozin:20211108225045p:plain

実際に該当のページに遷移すると、 JSON に格納したデータと一致するのを確認できます。

つまりページ遷移 = 読み込んだ JSON を JS でページの内容を書き換えるクライアントサイドレンダリングをしていると推測できます。

コードベースで言えば下記でビルド時にファイルパスを指定したマニフェストを作成し、ページロード時のコンテンツのパスを指定していると思われます。

https://github.com/vercel/next.js/blob/ad981783abbb347b6510c4dee5ce969e87ab0e24/packages/next/build/index.ts#L1725-L1734

https://github.com/vercel/next.js/blob/ad981783abbb347b6510c4dee5ce969e87ab0e24/packages/next/client/page-loader.ts#L141-L143

自作のサイトでも確認してみる

JSON の中の pageProps の構造について確証が持てなかったので自作のサイトでも確認してみることにしました。

https://nextjs-demo-spacex-crew.vercel.app/

自分のソースコードと比較すると、 pageProps 中の構造は Next.js の getStaticProps() で渡している構造と一致します。

つまり、ビルド時に渡したコンテンツのデータはJSONとして保存されていることが推測できます。 f:id:mr_ozin:20211108225109p:plain

https://github.com/ryokryok/nextjs-demo-spacex-crew/blob/b8812902315d0b111040f4980b1a70906faddcd0/pages/crews/%5Bid%5D.tsx#L30-L37

基本的にページコンテンツ必要になったら読み込む

ではブログの一覧画面のようなページが有り、リンク先が大量にあるようなケースでは読み込まれるJSONファイルの数が増えて、初期表示が遅れるのでは?と思いますが、その心配はありません。

Network タブを開いたまま、ページを上下にスクロールすると読み込まれる JSON ファイルが増えているのを確認できます。

ゆっくりスクロールするとわかりますが、ページのリンク が表示されたらその 遷移先のコンテンツのJSON が読み込まれます。

ユーザーが遷移先のリンクを閲覧する必要性が出てくるタイミングとして 「リンクが表示された段階」 を想定しており、そこで初めてJSONを読み込むように設計していることが推測されます。

リンクが表示された段階で既にコンテンツは読み込まれており、リンクをクリックするとクライアントサイドで書き換え、ページ内の画像やアセットの読み込みが実行されます。

これによって一気に読み込まれるJSONファイルの数を減らし、低速回線でも快適に閲覧できます。

コードベースで言えば next/link のコードを見ると Intersection Observer API を使って監視をしているのが確認できます。

(useIntersection という Hooks 経由で使用しています)

https://github.com/vercel/next.js/blob/4cdb585962b4f17be55848740d6e34106d730217/packages/next/client/link.tsx#L249

https://github.com/vercel/next.js/blob/4cdb585962b4f17be55848740d6e34106d730217/packages/next/client/use-intersection.tsx#L18

※ちょっと脱線しますが、 next/imageuseIntersection を使って監視しています。

https://github.com/vercel/next.js/blob/4cdb585962b4f17be55848740d6e34106d730217/packages/next/client/image.tsx#L438

最後に

Next.js は基本的にマニュアルどおりに作成すれば適宜最適化され、表示速度の早い Web サイトを簡単に作成できます。

素の軽い HTML ページに比べて、初回の表示は微妙に劣ることはありえますが、2ページ目以降は先読みした JSON を元にクライアントサイドレンダリングするのが基本のため、ページ切り替えが早いです。

コードリーディングした箇所についてはあまり自身がないので、間違っていたら指摘お願います。