爆速Headless NextJSのすゝめ

# はじめに

静的なページをインフラレベルで動的に生成し爆速にするためのエッセンスです。
ReactJS、NextJS、Vercelの挙動に関して知っている方向けの記事です。あしからず。

NextJS＆Vercelの機能をフル活用することでランディングページなどでReactJSを使ったモダンな開発をしつつ爆速な静的ページを生成することができます。
先日リリースされたNextJS 12.2.0から割と最後の課題であったVercelのページ単位のキャッシュクリアをAPIで解決することが可能になりました。

- SSG→ISR
- ページ単位のVercelキャッシュクリア
- CSR無効化


# ISR

ISRに関しては詳しくまとまっている記事があるのでそちらにゆずります。
[Next.jsにおけるSSG（静的サイト生成）とISRについて（自分の）限界まで丁寧に説明する](https://qiita.com/thesugar/items/47ec3d243d00ddd0b4ed#%E3%82%A4%E3%83%B3%E3%82%AF%E3%83%AA%E3%83%A1%E3%83%B3%E3%82%BF%E3%83%AB%E9%9D%99%E7%9A%84%E5%86%8D%E7%94%9F%E6%88%90-incremental-static-regeneration-isr)

端的に言うとキャッシュが切れたら動的にSSGを行う仕組みでVercelにはインフラレベルでこの仕組みが備わっています。
getStaticPropsなどで外部のAPIサーバからコンテンツデータ取得している際、ページ内コンテンツデータがしばしば流動的に差し替わる必要がある場合に導入します。（NextJSをHeadlessで使う）



## Using On-Demand Revalidate

https://nextjs.org/docs/basic-features/data-fetching/incremental-static-regeneration#using-on-demand-revalidation

NextJS 12.2.0の待望の新機能です。
要するに今までできなかったページ単位のVercelのキャッシュクリアを行うことが出来ます。
以下の手段でもISRページのキャッシュはクリアされますが痒いところに手が届かず不満点しかありませんでした。

- getStaticPropsのrevalidate時間指定→ただ時間経過を切れるのを待つのみ…能動的な対応ができない
- SWR→CSR側からfetchしないといけない。パフォーマンス的にも…
- 再デプロイ→全ページのキャッシュが切れる。getStaticPathで事前SSGしているとビルド遅くなるしページ数が多すぎるデプロイに時間がかかる、無駄も多い

API revalidateを行うために以下は必須です。

- React v 17.0.2以上
- Next 12.2.0以上
- ページのgetStaticPropsの定義（ISRのキャッシュクリアなのでISR該当ページでないと当然エラーになる）

実装自体はpages/api以下に[API](https://nextjs.org/docs/api-routes/introduction)を作成します。
revalidate関数で該当ページのISRキャッシュを消してページの再生成がされます。

```tsx
// src/pages/api/revalidate.ts
import { NextApiRequest, NextApiResponse } from 'next'

// Using On-Demand Revalidation
// https://vercel.com/docs/concepts/next.js/incremental-static-regeneration#using-on-demand-revalidation
export default async function handler(
  req: NextApiRequest,
  res: NextApiResponse
) {
  // revalidate path ex:) `/hoge/fuga`
  const path = req.query.path as string

  try {
    await res.revalidate(path)
    return res.json({ revalidated: true })
  } catch (err) {
    // If there was an error, Next.js will continue
    // to show the last successfully generated page
    return res
      .status(500)
      .send(`Error revalidating, ${(err as Error).toString()}`)
  }
}
```

検証用にゼロからサンプル作ってみました
https://vercel-revalidate.vercel.app/
サンプルでは以下のAPIをコールして正常に動作しているのを確認済み
ブラウザリロードと強制ページ再生成で比較
（レスポンスのVercel HeaderはHitだがきちんとgetStaticPropsが裏で呼ばれて更新されている。各ページに対してgetStaticPropsを呼んで更新する必要があるためdynamic routesに対してのワイルドカード指定は無理です。)
https://vercel-revalidate.vercel.app/api/revaliate
サンプルgit
https://github.com/teradonburi/vercel-revalidate

# CSR無効化（JSランタイムの無効化）

ページスピードのボトルネックとなるのはReactJSのCSRレンダリングの実行時間です。
特にコンテンツが多くDOM数が多いページほど如実にボトルネックとなります。
実際のところISRの時点で静的htmlのレンダリングはされている状態なので
CSR側でReactJSでSSR同様の仮想DOMを再構築するのはブラウザのレンダリング負荷的にも無駄な行為です。

[The 0kb Next.js blog](https://alistair.blog/zero-kb-nextjs-blog)

ページ単位で以下のunstable_runtimeJSフラグをfalseに指定するとCSRが無効になります。

```tsx
import {PageConfig} from 'next';

export const config: PageConfig = {
	unstable_runtimeJS: false,
};
```


ただし、完全な静的HTMLとして払い出しされるためにFE側のReactJSが無効化されてできなくなることがあります。

- つまりはonClickイベントやuseEffectなどのJSイベントハンドリングやReactのhooksなどが使えなくなります（GTMなどの計測トラッキングタグがどうしても必要な系は_document.tsxなどに直接scriptを埋め込む）
- next/router, next/link → 内部ルーティングを行うことができませんので使えません。aタグやformタグ（method=GET）に差し替えることで代用できます。Reactの疑似routingが使えないため、都度SSRされることとなります。
- next/image → CSR側でJS実行されるため使えません。imgタグで代用します
- FEのJSを無効にしているのでSWRなどのCSR側でのキャッシュクリアは使えません（新機能のUsing On-Demand Revalidateを使うことでAPIコールでの解決が可能になりました！）