💰

Link と ISR が引き起こす Next.js の過負荷

2021/05/12に公開

「なんだか Next.js の Static Generate に使っている外部 API 呼び出し回数が多いような?」と思っている方へ。閲覧されもしないページを、ISR(Incremental Static Regeneration)でみだりに再生成していませんか?本稿では、Link コンポーネントの振る舞いと ISG / ISR の組み合わせの際、注意したい prefetch の設定について言及します。

ISG のおさらい

ISG(Incremental Static Generation)は、Next.js がオンデマンドでページを静的生成するアプローチです。「オンデマンドで静的生成する」ことで、ビルドタイムの静的生成をスキップすることができます。

getStaticPathsfallback オプションを true'blocking' にすることで発動します。
https://nextjs.org/docs/basic-features/data-fetching#fallback-true

これは膨大な Dynamic route を提供するページであっても、静的生成を現実的なものとします。実際にページがリクエストされるまで、そのページは静的生成されません。ある種の SSR とも言うことができますが、ページが一度生成されてしまえば削除されるまで、再生成は起こりません。

ISR のおさらい

ISR(Incremental Static Regeneration)は ISG の「再生成が起こらない」課題を解決するアプローチです(データ陳腐化防止)。revalidate オプションを付与することで、一度生成したページであっても、指定経過時間後に再生成を試みます。

revalidate オプションの付与で ISG は ISR になるわけですが、あまりにも短い指定をすると、タイトルのような過負荷を引き起こすことが懸念されます。

Link と ISG の関係

そもそも、ISG が実行されるタイミングはいつでしょうか?「オンデマンド」と聞くと、1 番目はすぐに想像できると思いますが、2・3 番目は意外な事実かもしれません。

  • 1.ページが直叩きされたとき
  • 2.Link コンポーネントにマウスオーバーした時
  • 3.Link コンポーネントが画面内に入った時(prefetch={true}時)

この仕様は ISG の場合問題にはならず、投機的にページを静的生成してくれるのは歓迎できることです。しかし「一定間隔」を期に静的生成を試行する ISR の場合、問題になり得ます。

Link と ISR の関係

次の様なページがあった場合、どの様なことが起こるでしょうか?

  • Link コンポーネントが 30 個マウントされている
  • Link コンポーネントで遷移する先が「revalidate: 1
  • 30 個の Link コンポーネントがファーストビューに収まっている

正解は「このページを毎秒リロードすると、毎秒 30 ページ分の静的再生成を試行する」です。もし裏側で不本意に 30 倍 API が呼ばれていたのなら、prefetch 設定を見直しましょう。

Link コンポーネントの prefetch 設定は、default で true です。何気に Link コンポーネントを利用していると、ISR は「被リンクページが閲覧されただけ」で発動します。Link コンポーネントのリンク先が ISR の場合、prefetch={false} を指定することが無難でしょう。(もちろん revalidate の間隔が十分長ければ、この限りではないとは思います)

prefetch のトリガーになるタイミング

「Link コンポーネントが画面内に入った時」と書きましたが、正確には「画面外 200px 以内に入った時」です。useIntersection なる custom hooks の isVisible 変化を hook に prefetch が実行されます。Intersection Observer を使ったこの hook の効果で「スクロールで静的生成を実行する」ページが出来上がります。挙動としては、image の lazyload に似ていますね。

ISG / ISR で getStaticProps が実行される挙動は next dev ではなく next start で立ち上げたローカルの Next.js App でも確認できます。「Link コンポーネントが表示されそうな位置までスクロールされた」 だけで、再生成が試行される価値あるページなのか、ISR を利用する際には注意深く検討したいですね。

参考:https://github.com/vercel/next.js/blob/master/packages/next/client/link.tsx#L245-L271

const [setIntersectionRef, isVisible] = useIntersection({
  rootMargin: "200px",
});
const setRef = React.useCallback(
  (el: Element) => {
    setIntersectionRef(el);
    if (childRef) {
      if (typeof childRef === "function") childRef(el);
      else if (typeof childRef === "object") {
        childRef.current = el;
      }
    }
  },
  [childRef, setIntersectionRef]
);
useEffect(() => {
  const shouldPrefetch = isVisible && p && isLocalURL(href);
  const curLocale =
    typeof locale !== "undefined" ? locale : router && router.locale;
  const isPrefetched =
    prefetched[href + "%" + as + (curLocale ? "%" + curLocale : "")];
  if (shouldPrefetch && !isPrefetched) {
    prefetch(router, href, as, {
      locale: curLocale,
    });
  }
}, [as, href, isVisible, locale, p, router]);

Discussion