コミュニティ

Next.js 4年目の知見:SSRはもう古い、VercelにAPIサーバを置くな

ec.png

Next.js by Vercel - The React Framework

画像は Next.js サイコー!っていう顔です。

Webフロントエンドエンジニアであれば、「Reactのフレームワーク」と聞いて真っ先に思いつくであろうNext.js。僕は小規模の趣味開発から中規模の業務まで、4年程度Next.jsを使い続けてきました。触りはじめの当時はバージョン4で、”SSR(Server-side Rendering)を提供するReact製フレームワーク”だったものが、執筆時時点の最新バージョン(10.0.1)ではガラッと異なるフレームワークへと進化しています。

この4年間は実務で利用するだけでなく、新しいものや廃止された機能、RFC止まりになった機能など、Next.jsに関する情報を追いかけており、ある程度の知見をためつつも、Next.js並びに開発元のVercelが目指す方向性を何となくつかむことができたので、Next.jsとVercelの関係やそれらを踏まえた上での個人的ベストプラクティスをここにまとめておきます。

ちなみに「Next.jsってSSR用フレームワークだよね」「SSRするつもりないけど、Next.jsを使う意味あるの?」「Next.js久々に使うわ」「今Next.jsでSSRしているよ」という方々を読者として想定しています。

※記事最後に追記あり

おことわり

  1. 内容は、執筆時の最新版であるNext.js 10.0.1及びそれまでの変遷をベースとしたものです。
  2. 内容は、執筆者個人が得た経験に基づくもので、開発元であるVercel及びコミュニティとは一切関係ありません。
  3. プロジェクトの特性によっては内容が最適でない可能性もあります。現在運営中のNext.jsプロジェクトにおいては変更を慎重に検討し、また新たに開発する予定のNext.jsプロジェクトにおいては十分検証した上で参考にしてください。
  4. 本記事ではサービスとしての『Vercel』に関する情報も掲載しています。本来ならば別記事として分離できますが、これは3章で述べるNext.jsとの関係性から、あえて同一記事内に掲載しています。

1. Next.jsは、(もう)SSRするだけのフレームワークではない

2019年の後半から、日本でも業務へNext.jsを導入する話を多々聞くようになりましたが、利用経験はなくとも存在自体を数年前から知っている人も多いのではないでしょうか。数年前にNext.jsを触った人の多くは「SSR(Server-side Rendering)くらいしか特徴がないよね」「とても薄いフレームワークだよね」と思った人も多いことでしょう。しかし、今のNext.jsはあなたが予想しているものとはまったく異なるものになっているはずです。

Next.jsの開発は非常に活発で、おおよそ半年に1回の頻度でメジャーバージョンがリリースされてきました。その中でもNext.js 9.xは今までのNext.jsとは異なる開発体験、そして開発者に新しい道を与えてくれる革新的なリリースが続いています。

リリース日 バージョン 特に目立つ新しい機能/改善された機能
2020/10/27 10.0.0 Image コンポーネント追加/I18nルーティング対応/React 17対応/ import 文による外部CSSの読み込み対応
2020/07/28 9.5.0 Incremental Static Regeneration正式対応/ベースパス設定対応/レスポンスヘッダ設定対応
2020/05/12 9.4.0 Incremental Static Regeneration登場/新しい環境変数の読み込み方法/Fetch APIのUniversal Polyfill標準搭載
2020/05/10 9.3.0 次世代のStatic Site Generation登場/SassのCSS Modules標準対応
2020/01/16 9.2.0 CSS Modules標準対応/公式ドキュメント改善(GitHubからの README.md 削除 1
2020/01/07 9.1.7 Fetch APIのPolyfill標準搭載(Webブラウザのみ)/設定ファイルからReactのStrict Mode有効可能に
2019/10/08 9.1.0 pages ディレクトリを src 内に配置可能に/静的ファイル用 public ディレクトリ対応
2019/09/30 9.0.7 gzip圧縮配信に標準対応
2019/07/08 9.0.0 TypeScript標準対応/動的ルーティング対応/部分的な静的ページ生成対応/BFFにおけるWeb APIサーバの構築及びルーティング対応/Intersection Observerによるリンク先ページのプリフェッチ対応
2019/04/17 8.1.0 next/amp によるAMP対応
2019/02/11 8.0.0 Serverlessモード追加/ビルド時のみ利用する環境変数の定義が可能に/ <head> 要素内の重複を防ぐ仕組み追加/設定ファイルに corssOrigin オプション追加
2018/09/20 7.0.0 エラーレポート画面の改善/Dynamic Importsの独自実装を廃止/静的CDNを考慮した生成結果に変更
2018/06/28 6.1.0 .tsx など独自の拡張子を pages 内に配置可能に
2018/05/17 6.0.0 設定なしの静的生成対応/ _document.jsx 追加/TypeScriptトランスパイルをts-loaderからBabelへ変更
2018/03/27 5.1.0 環境変数をアプリケーション内で利用できる publicRuntimeConfig serverRuntimeConfig 追加
2018/02/06 5.0.0 Universal Webpack対応/プラグイン対応/プラグイン導入によるTypeScript対応
----/--/-- -.-.- 以下略

上記表は、ここ2年間にリリースされたバージョン5.0〜10.0までの変遷を簡潔にまとめたもの2で、Next.js 9.xの機能追加/改善の量は一目瞭然でしょう。事実、それまで半年に1回の頻度でメジャーアップグレードされていたNext.jsですが、バージョン9.xの時代は1年以上続いています。

Next.jsはバージョン9まで動的ルーティング3すら対応しておらず、SSRとSSG(Static Site Generation)しかできない微妙な立ち位置のフレームワークでした。しかし、React向けのSSGライブラリとしてはGatsbyの方が高機能で開発も活発であったため、Next.jsをSSGツールとして利用する人はあまりおらず、SSRするだけのフレームワークとして使っていたチームも多いことでしょう。結局のところ「ただSSRに対応するためだけの、薄っぺらいラッパーだよね」という印象は拭えませんでした。

しかしながら実際はSSR以外にも、webpackやBabelなどのビルド周りの最適化や、Chunks、Code Split、リンク先ページのプリフェッチなどパフォーマンスに関する汎用的かつ高効率のチューニングが徹底的に施されており、何も意識せずとも最大限のパフォーマンス最適化を受けられる点は、一貫して昔から変わっていません。むしろSSRくらいしか特徴的な機能がないNext.jsは、裏では本来かなりの工数を必要とするパフォーマンスチューニングが行われている、まさに“パフォーマンス改善の基盤に、SSRをオマケとして足した”ようなフレームワークだったわけです。

では最近のNext.jsはどうかというと、バージョン9.0から動的ルーティングに対応したり、BFF(Backends for Frontends)上で簡単なWeb APIを手軽に作成できるようになりました。加えて、バージョン9.3でリリースされた”次世代のSSG”とバージョン9.4のIncremental Static Regenerationによって、Next.jsはSSRではなくSSGに重きをおいたフレームワークへと舵を切っています。つまり現時点でのNext.jsの主な用途としては2つの選択肢があり、1つは“SSRに対応した汎用的なReact製フレームワーク”が、もう1つは“React製のSSGツール”が挙げられます

2. Next.jsでは、基本的にServer-side Renderingをするべきでない

前述の通り、Next.jsは元来SSRを特徴とするフレームワークだったわけですが、筆者はSSRを推奨していません。Next.jsに限らずSSRを避けるべき理由として、主に3つが挙げられるでしょう。

  1. SSR用のサーバを持たなければならない
  2. サーバサイドのセキュリティの危険性をWebフロントエンドエンジニアが抱えなければならない
  3. パフォーマンスの観点でもSEOの観点でも、SSGで十分、むしろSSGの方がより優れている

そもそも、サーバサイドに対する知見や経験の少ないWebフロントエンドエンジニアが、生半可な知識でサーバを建てるのは危険です。(サーバサイド)Node.jsは、Webフロントエンドと同じ言語であるJavaScriptで開発を進めることができますが、Webブラウザに存在するAPIのすべてをNode.jsで利用できるわけではありませんし、Webブラウザという一種のサンドボックスがない不慣れな環境での開発は脆弱性を生む可能性も大いにあります。今ではサーバレスアーキテクチャを謳うSaaSが増えており、従来のサーバ管理よりも格段に管理コストを軽減させることはできますが、結局Webフロントエンドエンジニアが得意としない領域を持つことのメリットはありません。サーバの管理というものは、なかなか難しいものです。

またパフォーマンス最適化の一環で近年利用されることの多い動的コンテンツのCDNによるキャッシュですが、キャッシュ設定のミスにより、認証後の秘匿情報が含まれたページがキャッシュされることによる個人情報の漏洩もたびたび問題になっています。メルカリの件LINEの件などが記憶に新しいですね。

つまり、サーバを持つこと自体がデメリットになりうるわけです。そこでNext.jsの開発元であるVercelは、SSGを推奨しています。Next.js 9以降からは特にSSG周りの機能を強化していますから、Vercelの思想がそのままNext.jsにあらわれていることは明らかです。なお、Next.jsでは静的生成の方法として、Static BuildとSSGという2種類の方法を設けています。Static BuildはコンポーネントなどをBabel/webpackを通して静的ファイルに変換するビルド、SSGはコンポーネント内で取り扱う具体的なデータを含めた静的ファイルに変換するビルド4を示しています。本記事では、それらの用語に準拠しています。

動的コンテンツをSSGする方法

❗️ドキュメントと同じ内容 この項では、動的コンテンツをNext.jsでSSGするための方法を簡単に解説しています。どれも公式ドキュメント以上のことは書いていないので、既に方法を熟知している人は読み飛ばしてください。

ケース1:動的コンテンツをSSGする

SSRを施す大きな理由の1つとして「動的コンテンツをSEOさせたい」点が挙げられます。Next.jsはバージョン9.3より getStaticProps と呼ばれるSSG専用のライフサイクルに対応しており、ビルド時にコンポーネントへ渡すPropsを確定させることができます。

ブログ記事一覧を表示するページの場合、この getStaticProps 内で記事一覧を取得するAPIを叩き、ページに表示するコンポーネントに記事データをProps経由で渡すことで、ビルド時に生成されるHTMLに記事データが含まれるようになります。

getStaticPropsを利用した例
// /pages/posts/index.jsx
import { Posts } from "../components/posts";

function Page(props) {
  return (
    <Posts posts={props.posts} />
  );
}

// SSGのビルド時のみ呼ばれるライフサイクル
//   -- ビルド時に記事一覧を取得するAPIを叩き、コンポーネントのPropsにその内容を渡す
//   -- その内容が渡されたコンポーネントからHTMLが生成されるため、記事データを文字列として含むHTMLが生成される
export async function getStaticProps() {
  const response = await fetch("https://api.example.com/posts");
  const json = await response.json();

  return {
    props: {
      posts: json.posts,
    },
  };
}

export default Page;

参考:Basic Features: Data Fetching | Next.js

ケース2:動的ルーティングが必要なページをSSGする

一方で、ブログ記事一覧ページではなく記事詳細ページのように、動的ルーティングを前提としたページをSSGしたい場面も当然あります。

Next.jsではバージョン9より動的ルーティングに対応しており、 pages ディレクトリ内でブラケット [] を囲ったディレクトリまたはファイルは動的パラメータとして扱うことができます。例えば /pages/posts/[postId].jsx というファイルは、 /posts/123 というURLに対応し、ページ内で扱うコンポーネントから 123 というパラメータを取得することができます5

では /posts/123 というURLにアクセスした場合、IDが 123 の記事を表示したい、でもSSGさせたい場合はどうすれば良いでしょうか。Next.jsは、これもまたバージョン9.3より対応した getStaticPaths というライフサイクルで、事前ビルド時に生成すべきURL(厳密には動的パラメータのみ)を列挙させ、対応するHTMLを一括生成させることができます。

getStaticPathsを利用した例
// /pages/posts/[postId].jsx
import { Post } from "../components/posts";

function Page(props) {
  return (
    <Post post={props.post} />
  );
}

// SSGのビルド時のみ呼ばれるライフサイクル
//   -- `getStaticProps` より前に実行される
//   -- 対応する動的パラメータを配列で返す
export async function getStaticPaths() {
  const response = await fetch("https://api.example.com/posts");
  const json = await response.json();

  const paths = json.map((post) => ({
    params: { postId: post.id },
  });

  return { paths };
}

// SSGのビルド時のみ呼ばれるライフサイクル
//   -- `getStaticPaths` より後に実行される
//   -- 引数には動的パラメータを含むコンテキストが渡される
export async function getStaticProps(context) {
  const postId = context.params.postId; // [postId]

  const response = await fetch(`https://api.example.com/posts/${postId}`);
  const json = await response.json();

  return {
    props: {
      post: json,
    },
  };
}

export default Page;

参考:Basic Features: Data Fetching | Next.js

getStaticPaths が返すオブジェクト(連想配列)には、パスを列挙した paths プロパティの他に、 fallback プロパティが用意されています。 fallback プロパティは真偽値を受け取り、事前ビルド時に列挙したパスとマッチしないアクセスが生じた場合に404エラーを返すかどうかを指定できるものです。 false にした場合は404ページを表示し、 true にした場合は、そのままコンポーネントをレンダリングします。コンポーネントでは useRouter というHooks API6を用いて isFallback オプションを参照することで、事前ビルド時のパスにマッチしない場合を条件分岐させることができます。これによって、ビルド後に追加されたコンテンツも、Webブラウザ上でオンデマンドにフェッチして表示させることができます7

繰り返しますが、これらはSSGの事前ビルド時のみ呼ばれるライフサイクルです。したがって、SSGのためのJSONを返すWeb APIエンドポイントを作るのも1つの手です。例えば今回のブログ記事詳細ページを生成する場合、事前にすべてのブログ記事のIDを取得できるAPIが必要になります。

ケース3:膨大な動的コンテンツをSSGする/事前ビルドなしで追加コンテンツをSSGする

ブログ記事の場合、多くても数千件の記事データをビルド時に取得すれば済むわけですが、例えばユーザによって生成されるコンテンツ──TwitterのようなSNSのページの場合、どうすれば良いでしょうか?数億、数兆規模のデータを事前ビルド時にフェッチしたりビルドすることなんてできません。

これは、Next.jsのバージョン9.4から登場し、バージョン9.5で安定版とされたIncremental Static Regeneration(ISR)を利用することで解決します。ISRは、動的コンテンツを“事前”ビルドせずに、初めてページにアクセスしたときにビルドするというものです。また、ビルドした内容には有効期限を設けることができ、有効期限を過ぎたページにアクセスされた場合は、前回ビルドされたコンテンツを返しつつも、バックグラウンドで再ビルドされることになります。CDNのキャッシュというよりも、Stale While Revalidateに近い挙動となります8

ISRを利用した例
// /pages/posts/index.jsx
import { Post } from "../components/posts";

function Page(props) {
  return (
    <Post post={props.post} />
  );
}

// SSGのビルド時のみ呼ばれるライフサイクル
//   -- ISRを利用する場合は `paths` を空配列にする
//   -- ISRを利用する場合は `fallback` を真にする
export async function getStaticPaths() {
  return {
    paths: [],
    fallback: true,
  };
}

// SSGのビルド時のみ呼ばれるライフサイクル
//   -- `revalidate` プロパティの値(秒)がビルド時からの有効期限となる
//   -- `revalidate` を記述することで、ISRが有効になり、“事前”ビルドは行われなくなる
export async function getStaticProps(context) {
  const postId = context.params.postId; // [postId]

  const response = await fetch(`https://api.example.com/posts/${postId}`);
  const json = await response.json();

  return {
    props: {
      post: json,
    },
    revalidate: 60,
  };
}

export default Page;

参考:Basic Features: Data Fetching | Next.js

ISRによって、膨大な動的コンテンツを含むWebサービスも、コンテンツが頻繁に追加されるWebサイトでも、静的ビルド(≠事前ビルド)させることが可能になるわけです。これは、他のJAMStack対応を謳うツールやサービスにはない、革新的な機能です。

今となっては、SSRのメリットはほとんどない

抱える動的コンテンツの量を問わず、あらゆるコンテンツを静的ビルドできるようになったNext.jsにおいて、SSRするメリットはほとんどありません。Vercelも述べるように、ビルドによって生成された静的コンテンツのキャッシュは、オンデマンドに生成される動的コンテンツのキャッシュと比べて、格段に容易になります。CDNとの相性もよく、エッジロケーションから直接クライアントまでコンテンツを配信できるようになります。さらに言ってしまえば、『Amazon S3』のようなオブジェクトストレージにビルドされたファイルを置くだけでデプロイが完了します。

静的ビルドされるということは、HTMLファイル内にコンテンツの内容が含まれることになります。これは(OGPのためも含む)SEOの面でも、パフォーマンスの面でも良い結果をもたらしてくれるでしょう。とりわけパフォーマンスの向上目的でSSRを導入しているプロジェクトにおいては、First Viewパフォーマンスの観点で事前ビルドに敵う余地はありません。

また「認証が必要なページのSSGはどうするの?9」と疑問に思うかもしれません。 getStaticProps から返すPropsに決め打ちでユーザIDなどを入力することは要件上できませんし、そのライフサイクル内でCookieを参照することもできません。すなわち、認証が必要なページのSSGはできないわけです。逆に、認証が必要なページのSSGは不要と言えるでしょう。その代わり getStaticProps などを用いないStatic Build─認証処理はWebブラウザでオンデマンドに処理して、事前ビルド時にはコンポーネント構造だけをHTML化する─という方法があります。Next.jsにおける、従来のSSGです。

そもそも認証が必要なページには秘匿情報が含まれていますから、SEOさせる必要がありません。秘匿情報をHTMLに埋め込まなければ、検索エンジンのクローラに収集されることもありませんし、CDNによる個人情報のキャッシュミスなども起こりません。コンテンツが埋め込まれていない状態でのコンポーネントの静的ビルドは済んでいますから、First Viewパフォーマンスに対する影響も大きくないはずです。


Next.jsではAutomatic Static Optimizationと呼ばれる、ページ単位でのSSR/SSGに対応しています。1つのプロジェクトでSSRとSSGのどちらにも対応できるというのは、これまたNext.jsの強みではありますが、特殊な事情や要件が絡んでくるページ以外では、SSGの採用を強く推奨します。

余談:SSRは歴史上に置いて、不要な産物だったのか?

では「SSRとは何だったのか、我々は遠回りしてきたのか」と思うかもしれません。これは誤りで、SSGはSSRありきの技術です。

SSRは5年以上前まで、ヘッドレスブラウザによるDOMノードの解釈が主流でした。Angular.jsがWebフロントエンドの主力フレームワークだった当時、SEOのためにSSRをする必要がありましたが、Prerenderなどの外部サービスを用いるか、自前でヘッドレスブラウザを起動してHTMLファイルを生成する必要があったのです。

React──特にVirtual DOMの登場によって、SSRには2つ目の方法が生まれました。それが、DOMノードというWebブラウザでしか扱えない構造体を抽象化することによって、ヘッドレスブラウザなしでSSRする方法です。厳密には、“レンダリング”ではなく、Virtual DOM(≒JSX)のHTML化と呼ぶほうが正しいでしょう。またReactは、HTMLファイルが持つDOMノードの構造体からReact内のVirtual DOMとして再利用する“Hydration”に対応したことで、Virtual DOMとHTMLの相互変換を可能にしています。今日では、Reactだけでなく、AngularやVue.jsでも似たようなことを行うことができるようになっています。

SSGはSSRの技術なしでは実現できないものです。なぜなら、SSGによるビルドは、SSRにあたる“レンダリング”を事前に行うか、オンデマンドに行うかの違いでしかないからです。都度リクエストを受け取ってHTMLを生成するのがSSR、デプロイ前にHTMLを生成するのがSSGとなるわけですね。

ちなみに、Node.jsではWHATWGやW3Cが提唱するJavaScript用のAPIにはあまり対応していません。例えば、XHRに変わるFetch APIが代表的なものの1つになります。そのためNext.jsの getStaticProps などといったSSG用ライフサイクルではFetch APIのPolyfillが必要になる……と思いきや、Next.js 9.4からはNode.js向けに fetch のPolyfillが標準搭載されている10ので、Polyfillを別途追加する必要はありません。

3. Next.jsは、『Vercel』でのホスティング前提のフレームワークだと認識する

image.png

Develop. Preview. Ship. For the best frontend teams – Vercel

Next.jsは革新的な機能をいくつも搭載しています。先述したように、Next.js 9.4からは膨大なコンテンツでも静的ビルドが可能なISRが登場しましたし、先日リリースされたばかりのNext.js 10.0ではオンデマンドでの画像最適化(WebP生成やリサイズ)に対応したため、APIで引っ張ってきたURLの画像ファイルすら対応するというスグレモノです。

一方でこれらの機能はNext.js単体で完結するかと言うと、そうではありません。2章では散々「Webフロントエンドエンジニアがサーバを持つな」なんて述べましたが、結局サーバなしではISRによる静的ビルドも画像最適化のための画像生成も行うことはできません。そこで、必然的に利用すべきサービスとなるのが『Vercel(ヴァーセル)11です。

『Vercel(サービス名)』は、Vercel(企業名)によって運営/開発されている、FaaS(Function as a Service)です。DNSやCDNサービスも展開しているため、PaaS(Platform as a Service)と言っても良いかもしれません。16ものエッジロケーションを備えるCDNやサーバレスファンクションは、AWSまたはGCPなどの大手クラウドコンピューティングプラットフォームのリソースを抽象化したもので、利用者がAWSやGCPを意識することはほとんどありません12。また従来の“サーバレスアーキテクチャ”を謳うSaaSとは異なり、サーバのスペックを選択できなかったり、ほとんどの設定が自動で行われ、かつ無効にすることができなかったりと、サーバの存在がほとんど隠蔽されているサービスでもあります。

Next.jsは『Vercel』と併用して初めて“完成”するフレームワーク

Next.jsと『Vercel』の組み合わせは、この上なく快適なものとなります。ページ単位のCode Splitによって分割されたファイルは『Vercel』上のサーバレスファンクションに分散配置され、静的ファイルは別途キャッシュ可能なストレージへデプロイ/CDNによるキャッシュに対応するほか、先述したISRも画像最適化も、すべて一切の記述やマウスクリックなしで自動的に設定されます。

「Next.jsの一部の機能は、『Vercel』を使わないと利用できないの?」と思うかもしれません。そしてその答えは、「事実上、そうです」となります。もちろんNext.jsのホスティングは『Vercel』以外でも利用できるようになっており、例えば、画像最適化に関しては、ImgixAkamaiなどの他サービスを利用する設定を備えています13。しかし、ISRに対応したプラットフォームは『Vercel』しか存在しません(執筆時時点)し、今後もNext.jsの新機能にいち早く対応するサービスは『Vercel』のみでしょう。

Next.jsがOSSとは言え、Vercelという企業がNext.jsを主に開発している以上、『Vercel』ファーストな設計になるのは当然であり、現状『Vercel』のSDKになっていると言っても良いでしょう。むしろ、Next.jsは『Vecel』というサービスを利用して初めて完成するフレームワークであると言えます。Next.jsを使うということは、“『Vercel』に依存する”ということが前提になることを強く意識してください

それでもNext.jsを他プラットフォームでホスティングしたい場合

Next.js 9より対応したAutomatic Static Optimizationによって、以前よりも、より一層自前デプロイが難しくなりました。SSRのためのBFFを必要とするページと、サーバを必要としない静的ファイルを適切な場所へデプロイしてくれるのは『Vercel』ならではです。こういった恩恵をすべて捨ててまで、他のプラットフォームでホスティングする場合、以下のことを意識しておくとデプロイ環境の構築も少しは楽になるかもしれません。

  • “1ページもSSRしない”、または“1ページも静的ビルドしない”のどちらかに舵を切る
    • SSRを一切しない場合、 next build ではなく next export で静的ファイルを生成する
    • SSRしか利用しない場合、 next.config.jstarget: "server" モードを有効にする
  • AWSにロックインする場合、Serverless Next.jsを利用する

まず、最初からAutomatic Static Optimizationに対応したデプロイ環境を用意するのはとても大変です。したがって、プロジェクトのルールとしてSSRを一切使わないか、SSRしか使わないかの2択に絞ってしまうのが良いでしょう。個人的には2章で取り上げた内容の通り、SSRが本当に必要な環境は極めて限定的だと思われるため、一切SSRしないルールを導入することを推奨しておきます。

またSSRする場合、サーバレスファンクションを提供するサービスの容量制限にも注意が必要です。『AWS Lambda』の場合は250MBのデプロイパッケージに対する容量制限が存在するため、 node_modules で枯渇してしまう可能性が大いにあります。ここらへんのリソースをどのように分散させるのかは、頭を悩ませる大きな要因となるでしょう。

余談:1ページ1サーバレスファンクションの戦略

Next.jsはページ単位でのCode Split(コード分割)に対応しており、1ページごとにJavaScriptファイルが(実際には複数の関連ファイルもセットで)作成されます。これによって、初回アクセス時に当該ページ以外の情報を読み込む必要がなくなるため、First Viewパフォーマンスの向上に繋がります。

Vercelではこの“ページ”という単位を1つのサーバレスファンクションに配置させる戦略をとっています。これにはいくつか理由があります。

まず、サーバレスファンクションを謳うサービスの多くは、負荷に応じて自動的にサーバ台数を増減し、また一定期間アクセスがない場合はスリープ状態に入るようになっています。スリープ状態のサーバに対して再びアクセスされた際には、スリープから自動的に復帰します(これをコールドスタートと言います)が、この復帰にはユーザ体験を損ねる程度の時間を必要としてしまいます。コールドスタートの時間を削減する方法はいくつかありますが、代表的なものはサーバが抱えるプログラム容量の削減が挙げられます。Vercelでは、Next.jsの1ページ分を1サーバへ配置させることで、コールドスタートの時間を極力小さくしようとする設計を取っています。

また、1サーバあたりの用途を“特定ページのレンダリングのみを担う”という形で絞ることによって、システム全体の負荷を分散させるという目的もあります。一般的に高い負荷をさばくためには、高性能な1台のサーバを用意するスケールアップよりも、そこそこの性能のサーバを複数台用意スケールアウトの方が費用対効果が高いとされています。Vercelという事業者側の目線でも、1台のサーバレスファンクションにすべてのソースコードを詰め込む理由はないわけですね。

4. Next.jsでAPIサーバを建てない/『Vercel』でAPIサーバを建てない

Next.jsはバージョン9より、APIルーティングにも対応しました。これは、Next.js内でNode.jsのWeb APIサーバを簡単に作成できるというものです。

APIルーティングの例
// /pages/api/hello.js

// `/api/hello` に対してのAPIコールで `Hello` が返ってくる
export default function handle(req, res) {
  res.end("Hello");
}

このAPIルーティングは、同じくバージョン9で登場した動的ルーティングにも対応しているため、 /pages/api/users/[userId].js というファイルを作れば、 /api/users/123 のURLに対応したAPIを実装できることになります。

Next.jsがAPIルーティングに対応したことで、WebフロントからサーバまですべてNext.js内で完結させることも可能になったわけですが、これは次に述べる観点から避けるべきであると考えています。

Web APIフレームワークではないので、機能的に貧弱

Next.jsはあくまでReactを用いること前提のフレームワークであり、APIルーティングはおまけ程度のものです。リクエストメソッドのフィルタリング、リクエストボディのバリデーションなどもすべて自前で記述しなければならないため、本格的なWeb APIサーバを開発するには向きません。

サーバサイドNode.jsに特化したフレームワーク『NestJS』や、Next.jsを拡張したフルスタックフレームワーク『Blitz.js』を利用すると良いでしょう。

サーバレスファンクションとRDSの相性が悪い

Next.jsはサーバレスファンクションを前提とした設計になっており、大量の台数までスケールアウトされるサーバレスファンクションと、サーバサイドでよく使われるRDBMSは、コネクション数の点で相性の悪いことが知られています。

AWSのDBaaSにおいては、最近は『Amazon RDS Proxy』の登場によって『AWS Lambda』と『Amazon RDS』のコネクション数問題がある程度解決できるようになりましたが、依然としてサービスを選ぶことになります。一方でNoSQLやKVSをサーバサイドのプライマリデータベースとして採用することもできますが、RDBMSを採用しておいた方が柔軟で安定した開発/運用を継続できるため、よほどの理由がない限り避けておいたほうが良いでしょう。

『Vercel』のサーバレスファンクションと帯域制限の相性が悪い

『Vercel』が定めるFiar Use Policyによると、プランごとに帯域幅制限があり、Proプランの場合でも毎月1TBまでしか許可されていません。1TBと聞くと結構な量のように思えますが、ファイルアップロードが可能なサービスの場合は、簡単に帯域制限の上限に到達してしまうので、別途Webブラウザ上から直接他のストレージへアップロード処理を施さなければいけません。この場合、当該ストレージとデータベースのトランザクション管理が必要になるため、実装/管理コストを考慮する必要があります。

余談:『Vercel』のEnterpriseプランを契約すれば解決?

サーバレスファンクションや帯域制限の上限に到達してしまった場合、プランのアップグレードが必要になりますが、既にProプランの場合はEnterpriseプランしか選択肢がありません。Proプランが毎月1ユーザあたり20ドルなので、Enterpriseプランは倍程度の40ドルを払えば契約できるだろう……と思う人もいるかもしれません。

『Vercel』が用意するEnterpriseプランは、まさに中企業大企業向けのプランであり、Enterpriseプランは月ではなく年契約が必要なプランになっています。手軽にクレジットカード払いで契約できない『Heroku Enterprise』に近い形態ですね。『Vercel』のEnterpriseプランの具体的な金額は伏せますが、最低でも年間数万ドルと、個人開発者やベンチャー企業が手軽に支払える額ではありません。一方でEnterpriseプランの最大の強みは、プロジェクトに合わせて個別に制限緩和してくれたり、迅速かつ詳細な技術サポートを受けられるというメリットもあります。

そもそも『Vercel』にはDBaaSやオブジェクトストレージサービスは存在せず、何らかのWebサービスを開発しようとなると、別途FaunaDBなどのSaaSを利用せざるを得ません。特にセキュリティに意識しなければいけない業務では、データベースサーバをプライベートネットワークに置くことが望ましいため、結局APIサーバとデータベースサーバを同じく1つのプラットフォームで管理できるPaaSを利用することになるでしょう。


結局のところ、Next.jsにおけるAPIルーティングは出番の多い機能というわけではなく、別途用意したWeb APIサーバをラップするかのような簡易版BFFとしてしか利用できないのが現状です。何か良い使い道があれば、教えてください!

5. 『Vercel』を使うなら next dev ではなく vercel dev を用いる

Next.jsを『Vercel』上で動かすことが確定している場合、ローカル環境での開発は next dev コマンドではなく vercel dev コマンドを使いましょう

vercel devnext dev と同じくHMRなどのリロード機構を備えていますし、 vercel.json の設定に応じて『Vercel』の挙動をエミュレートしてくれます。さらに今年の4月にリリースされたVercel CLIの vercel env pull コマンドを利用することで、『Vercel』の管理画面から設定した開発時向け環境変数をプルすることができます。

『Vercel』は「Develop. Preview. Ship.」をサービス理念として掲げていますが、『Vercel』を利用する以上、このレールに沿っておくのがベターとなります。このうちの“Develop.”が vercel dev を始めとする、開発環境でも『Vercel』に依存することにあたります。

開発環境では常にSSRされることに留意する

Next.jsを使う以上、 next dev であろうが vercel dev であろうが、開発環境では常にSSRされることに留意してください。これはSSGを前提に開発を進めている場合でも同様です。2章でも記述した通り、SSGはSSRあっての技術です。すなわち、SSGであってもNext.jsを利用する以上は、Universal JavaScriptを意識しなければいけません。

一方で、Next.jsはReactのライフサイクルを無視するような構造にはなっていないため、サーバサイド(Node.js)でどのライフサイクルが呼ばれないのかは自前でのReact Server-side Renderingと同様です。 componentDidMountuseEffect などはサーバサイドで呼ばれません。この中で直接 window オブジェクトを参照するのか、または if などによる分岐でearly-returnによる安全な参照を心がけるのかはプロジェクトのルールによるところでしょう。

6. Link で正しくリンクを貼る方法を知る

next/link がエクスポートする Link コンポーネントは、同じNext.jsプロジェクト内の他ページへリンクする際に用いるものですが、子コンポーネントに a 要素以外を置く場合は、いくつかのPropsを追加しなければいけません。またリンクを張った際に、意図したリンクの貼り方になっているかを確認する必要があります。

Link 内に置く要素/コンポーネントに応じて必要になるPropsを知る

❗️ドキュメントと同じ内容 この項では、Next.jsでリンクを貼る方法を簡単に解説しています。どれも公式ドキュメント以上のことは書いていないので、既に方法を熟知している人は読み飛ばしてください。

どのPropsが必要になるかの条件は、Next.js 9から簡素化されました。Next.js 9以降に必要となるPropsは以下の通りです。

Link 内に直接要素を置く場合
// `a` 要素の場合
<Link href="/posts">
  <a>記事一覧へ</a>
</Link>

// `a` 以外の要素の場合
<Link href="/posts">
  <div>記事一覧へ</div>
</Link>

Link コンポーネント内に a 要素を直接置く場合は、特に追加の手順を必要としません。 a 要素を置く場合、 hrefLink コンポーネントに渡します。

Link 内にコンポーネントを置く場合(最上層要素が a 要素)
// リンクを貼る側のコンポーネント
<Link href="/posts" passHref>
  <Button>記事一覧へ</Button>
</Link>

// リンク内に配置されるコンポーネント
const Button = React.forwardRef((props, ref) => (
  <a ref={ref} href={props.href} onClick={props.onClick}>
    {props.children}
  </a>
));

Link コンポーネント内に a 要素を最上層に持つコンポーネントを置く場合、当該コンポーネントに forwardRef を通して ref を受け渡した上で、以下のPropsを a 要素へ渡す必要があります。

  • href ... string | undefined
  • onClick ... ((event: React.MouseEvent<HTMLAnchorElement, MouseEvent>) => void) | undefined

加えて Link コンポーネントの passHref Propsを true にします。これはstyled-componentsなどの、コンポーネントを生成するタイプのCSS in JSを利用する場合も同様になります。

styled-componentsのリンクの例
const Button = styled.a`
  font-size: 14px;
`;

<Link href="/posts" passHref>
  <Button>記事一覧へ</Button>
</Link>
Link 内にコンポーネントを置く場合(最上層要素が a 以外の要素)
// リンクを貼る側のコンポーネント
<Link href="/posts" passHref>
  <Button>記事一覧へ</Button>
</Link>

// リンク内に配置されるコンポーネント
const Button = React.forwardRef((props, ref) => (
  <a ref={ref} onClick={props.onClick}>
    {props.children}
  </a>
));

Link コンポーネント内に a 以外の要素を最上層に持つコンポーネントを置く場合、当該コンポーネントに forwardRef を通して ref を受け渡した上で、以下のPropsを a 要素へ渡す必要があります。

  • onClick ... ((event: React.MouseEvent<T, MouseEvent>) => void) | undefined
    • T は最上層要素の型

実際に生成されるHTMLの a 要素に href 属性が設定されているか確認する

Link コンポーネントを利用すると、基本的には onClick によるクリックイベント発火時にHistory APIを用いてページ遷移が行われます。この際、Webブラウザのネイティブページ遷移は用いられず、また href 属性の値も利用されることはありません。そのため a 以外の要素でも、要素クリック時にページ遷移が可能になるわけです。

一方で、この挙動が小さなバグの温床にもなります。例えば Link コンポーネントに passHref Propsを指定しなければいけない場面や、内包されるコンポーネントの a 要素にProps経由で href を渡さなければいけない場面において、それらを怠った場合、 a 要素の href 属性は設定されません。この状況でリンクをクリックした場合History APIによるページ遷移が可能であるため、リンクを正常に貼れたと思いこんでしまいがちですが、実際には href 属性がないため、オンマウス時にステータスバーに遷移先URLが表示されなかったり、リンクを新規タブで開くことができなくなってしまいます。これはユーザビリティの低下やSEOにも悪い影響を与えるので、リンクを張った際は必ずページ遷移可能か、そして生成されたHTMLの a 要素に href 属性が設定されているかを確認しましょう。

余談:Next.jsのページ遷移の仕組み

Next.jsでは、以下の仕組みでページ遷移が行われています。

  1. リンクの onClick によるクリックイベント発火時に、遷移先ページのJavaScriptファイルの取得をXHRで試行する
  2. 遷移先ページのJavaScriptファイルが存在する(ステータス200)場合、 eval によってJavaScriptが実行され、DOMノードの書き換えとHistory APIによる履歴操作が行われる
  3. 遷移先ページのJavaScriptファイルが存在しない(ステータス404)場合、 window.location.href によるネイティブ遷移が行われる

手順1によるXHRの取得先は /_next/static/chunks/pages/<遷移先ページファイル名>.js となっており、手順2において意図しないJavaScriptファイルが eval によって実行されることはありません。ただし、遷移先ページのJavaScriptファイルが存在しない場合でも、必ず一度はXHRによって遷移先ページのJavaScriptファイルを取得しようと試みるため、外部サイトへのリンクに Link コンポーネントを利用することは不適と言えます。特にSentryなどのWebフロントエンド用ログ収集サービスを用いている場合は、XHRにおける404のエラーログが大量に収集されることになるため注意が必要です。

7. webpackの設定には極力触れない、Zero Configをできる限り保つ

Vercelは、『Vercel』にもNext.jsにも、Zero Configに大きく力を入れています。Next.js 8まではCSS ModulesにしろTypeScriptにしろ、何かしらの開発環境を整えるために設定ファイル経由でwebpackをいじる必要がありましたが、Next.js 9以降はその必要もなくなりました。

Next.jsに限らず、Webフロントエンドのフレームワークの多くはwebpackやBabelに依存しており、これらは複雑に設定されています。特にNext.jsにおいては、パフォーマンスの観点でかなり考え抜かれた設定になっています。Next.jsでは next.config.js ファイルからwebpackの設定オブジェクトにアクセスできますが、できる限り触るべきではありません。GitHub Issuesにも挙がっていないような挙動が生じたり、バージョンアップが困難な状況に陥る可能性が高まるからです。

Custom Serverを避ける

個人的に、SSRと同じくらい利用を避けたいと思っているものが、Custom Serverです。

Next.jsは古くから、Express.jsによってNext.jsを利用可能な構造になっています。Express.jsで受け取ったリクエストをNext.jsに流すことで、Next.jsを使いつつもExpress.js向けの拡張機能を利用することができる、というものです。これをCustom Serverと呼びます。

Next.js 8まではページの動的ルーティングに対応していなかったため、next-routesのようなCustom Serverを強いるパッケージを利用する必要がありました。また静的ファイルの配信やコンテンツのgzip圧縮やセキュリティに関するヘッダを設定するための方法も、Custom Serverで行うことが一般的でした。

Next.js 9.0.7からはデフォルトでgzip圧縮に、Next.js 9.1からは public ディレクトリによる静的ファイルの配信に対応し、Next.js 9.5からは next.config.jsheaders プロパティを設定することによって、SSR/SSGを問わず、レスポンスヘッダを事前に指定できるようになりました。開発元のVercelもCustom Serverの利用を推奨しておらず、これらのアップデートはCustom Server廃止を促す活動の一環でもあります。そもそも、SSGを推奨するNext.jsにおいて、SSGと相性の悪いCustom Server14を推す理由はありません。

8. バージョンアップ時のZero Breaking Changesを鵜呑みにしない

Next.jsは昔からずっと、Zero Brekaing Changesを掲げてバージョンアップされてきました。新しい機能を追加する際も後方互換性を考慮したAPI設計になっているなど、活発ながらも慎重に開発が進められています。リリースノートでもこの点は強調されており、比較的バージョンアップのしやすいフレームワークと言えるでしょう。

一方で、現場のエンジニアにとっては当たり前ではありますが、フレームワークに限らずライブラリのアップデート時は必ず動作確認を行わなければいけません。これはZero Breaking Changesを掲げているNext.jsも同様です。Next.js 4から利用している身としては、バージョンアップデート時に何度か破壊的変更に悩まされたことがあります。記憶に新しいものとしては、Next.js 9から Link コンポーネントで囲われるコンポーネントに対して forwardRef が必要となる条件変更などが挙げられます。Next.jsのレールから外れていない場合においても、公式ブログには記述されていない破壊的変更によって影響を受ける可能性は十分にあります。特にリンクが正しく動作するかは、ビジュアルリグレッションテスト等では気付かないため、E2Eテストまたは手動による動作検証が不可欠です。

Next.jsのレールから極力外れないことを念頭に置き、バージョンアップ時は必ず動作確認を行うことが必要です。特に _app.js_document.js をカスタムしている場合は変更作業を強いられることが多く、またこれらのファイルはすべてのページに影響があるため一層注意が必要となります。

9. ページコンポーネントには、ロジックを書かない

Next.jsでは /pages 以下にJSXファイルを置くことによって、ルーティングと表示させるコンテンツを一度に設定させることができますが、プロジェクトの規模や要件が複雑になるにつれて、かえってこの“ルーティングと表示コンテンツの密結合”な仕様が煩わしく感じるようになります。

ルーティングと表示コンテンツをできる限り疎結合にするために、ページコンポーネントではコンポーネントを読み込むだけに留めて置くのが吉です。

/pages/index.jsx
import { Top } from "../components/top";

function Page() {
  // Hooksなどは用いずに、コンポーネントを読み込むだけに留める
  // Hooksを使いたい場合は、子コンポーネント内で使う
  return <Top />;
}

export default Page;

「サブドメインがxxxのときは、違うページを表示させたい」などといった、複雑なルーティングを必要とする要件が生じた場合は、ルーティングに関する記述のみをページコンポーネントに記述します。

/pages/index.jsx
import { Top } from "../components/top";
import { MembersTop } from "../components/members";

function Page() {
  // ルーティングに関する処理の記述は許容する
  const router = useRouter();
  const subdomain = getSubdomain(router);

  // サブドメインが存在する場合は、異なるコンポーネントを表示
  if (subdomain != undefined) return <MembersTop />;

  return <Top />;
}

export default Page;

ファイルパスベースのルーティングシステムは直感的な反面、複雑なことを行おうとすると少々トリッキーな方法が必要になってしまうのが欠点となります。将来のバージョンで、ルーティング設定ファイルなどを追加できれば良いのですが……。もっとも上記のような例の場合、別リポジトリもしくは別Next.jsプロジェクトとして立ち上げたほうがシンプルに解決できるでしょう。

余談:個人的に落ち着きつつあるディレクトリ構造

SPAと言えども、 mainh1 などの要素を用いてセマンティックなHTMLが生成されるように心がけたいものです。とはいえこれらの要素を再利用性の高い汎用的なコンポーネントで使うことの相性の悪さは想像に難くありません。また上述した通り、ルーティング以外の処理をページコンポーネントに記述することは避けておきたいため、これらの要素をページコンポーネントで使うことも避けたい状況です。

そこで僕がNext.jsを使う場合、 foundations layouts components というディレクトリを追加しています。

+ src/
  + pages/       ... ページコンポーネント(Next.js標準)
  + foundations/ ... ページ全体に関わるロジックを含むコンポーネントたち(認証処理/スプラッシュ画面など)
  + layouts/     ... ページ全体のUIに関わるコンポーネントたち(ヘッダやフッタなど)
  + components/  ... 再利用性の高いコンポーネントたち

慣れているのであれば、Atomic Designのようなアーキテクチャを採用しても良いでしょう。いずれにしろ、ルーティングのロジック、 main などといったページ全体に影響するコンポーネント、再利用性の高いコンポーネントを明確に分け、これらに違反しない一貫したプロジェクトルールを定めることが必要となります。

10. meta 要素には key Propをつける

Next.jsでは next/head による Head コンポーネントを用いることで、ページ内のどのコンポーネントからでもタイトルや meta 要素を設定することができます。React Helmetに似たコンポーネントとなっています。一方で、同一ページ内に複数の同一 meta 要素が存在する場合、重複した分だけHTMLに含まれてしまいます。

これらの重複分を削除するために、Head コンポーネントはNext.js 8から key Propによる重複削除に対応しています。 key Prop付きの meta 要素が同一ページ内に登場した場合、最後にレンダリングされた要素のみが残るというものです。

そもそも、あらゆる場所で Head コンポーネントを利用してページタイトル等を変更すること自体が望ましくありませんが、意図しない重複タグを産まないためにも、 meta 要素には必ず key Propを指定するルールを設けておくと良いでしょう。なお、個人的には /pages 以下のページコンポーネント、もしくは先述したディレクトリ構造における /layouts 内のコンポーネントでのみ Head を利用することをオススメします。

_document.jsHead に注意

_document.jsのHead
import Document, { Html, Head, Main, NextScript } from 'next/document'

class MyDocument extends Document {
  static async getInitialProps(ctx) {
    const initialProps = await Document.getInitialProps(ctx)
    return { ...initialProps }
  }

  render() {
    return (
      <Html>
        <Head />
        <body>
          <Main />
          <NextScript />
        </body>
      </Html>
    )
  }
}

export default MyDocument

すべてのページのDOMノード構造を変更したい場合に使われる _document.js でも Head コンポーネントが利用されていますが、ここで利用される Headnext/document からエクスポートされたコンポーネントであることに注意してください。一方、他ページで利用する Headnext/head でエクスポートされるものです。

加えて _document.jsHead には title などの一部の meta 要素を置くことができません。これらの要素を _document.js に記述した場合、レンダリングに関する不具合の発生や先述の重複排除が有効にならないため、エラーが表示されます。すべてのページに対してデフォルトのページメタ情報を設定したい場合は、 _document.js ではなく _app.js に記述すると良いでしょう。

_app.js
function MyApp = ({ Component, pageProps }: AppProps) => (
  <>
    <Head>
      <meta key="viewport" name="viewport" content="width=device-width,initial-scale=1.0,viewport-fit=cover" />
      <title key="title">{defaultWebSiteName}</title>
    </Head>

    <Component {...pageProps} />
  </>
);

export default MyApp;

11. ルーティングとリンク先を一元管理するファイルを作成する

Next.jsはファイルパスベースのルーティングシステムを採用していますが、リンクの href に渡す値は文字列でなければいけません。 /pages 以下のファイルパスが変わってしまった場合、それらのリンクは機能しなくなり、またLinterによるチェックも通ってしまうため、ルーティング情報を一元管理しておくことをオススメします。

簡易的なルーティング一元管理方法
// routes.js
export const routes = {
  top: "/",
  users: "/users",
  usersUser: (userId) => `/users/${userId}`,
};

// コンポーネントでの利用
export const Foo = (props) => (
  <section>
    <Link href={routes.usersUser(props.userId)}>
      <a>ユーザ情報</a>
    </Link>
  </section>
);

なおNext.js 9.5.2以前までは Link コンポーネントに対して as Propを与える必要がありましたが、Next.js 9.5.3からは不要になりました15

余談: 幻の useLink

上述した方法は、 /pages 以下に存在するファイルと同期されるわけではないので、ページファイルを変更したときはルーティングファイルも変更しなければならないという運用が必要になります。結局、ページの二重管理になってしまっているわけですね。

幻のuseLink
// pages/index.js
import { useLink } from "next/link";

function Home() {
  const AboutLink = useLink("/about", () => import("./about"));

  return (
    <>
      Go to about page
      <AboutLink>About</AboutLink>
    </>
  );
}

[RFC] useLink · Issue #7329 · vercel/next.js

Next.jsでは、過去に import() を用いたページファイルの読み込みでリンクを貼る useLink というHooks APIが提案されていますが、Reactチームのメンバーによっていくつかの欠点が指摘されたため、お蔵入りになっています。しかし、こういった提案がNext.jsのコアメンバーから出されたということは、この件に関して問題視しているため、いずれ違う形で解決されることに期待できそうです。

12. Reduxを用いる場合は、Hydrationのマージ方法に留意する

❗️Reduxユーザ向け この章は既にNext.jsでReduxを用いる環境をターゲットにしています。これからNext.jsを用いて何らかのプロジェクトを立ち上げる場合は、強い理由がない限り、状態管理アーキテクチャとしてReduxを採用することを推奨しません。多くの場合、jotaiのように、もっとシンプルな状態管理ライブラリで、少し攻めるならばFacebookによるRecoilで解決します。

高頻度で書き込みが行われる複数の状態を同一ページ内の複数箇所で参照するような状況では、依然として多くの恩恵を受けられるReduxですが、Next.js 9.3から強化されたSSG機能に伴って、Next.jsにおけるReduxの扱い方に気を配る必要が生じました。具体的には、Next.js 9.3から追加されたライフサイクルと、ReduxのStoreをよしなにHydrationしてくれるnext-redux-wrapperのバージョン6の変更が大きく影響しています。

next-redux-wrapperは、SSR時に生成されたRedux Store内の状態を、ページレンダリング後にクライアントに引き継がせる“Hydration”をうまく処理してくれるライブラリでした。しかしNext.js 9.3からは、事前ビルド時にコンテンツを含めるSSG向けライフサイクルが登場したこと、そしてページ単位でSSR/SSGを設定できるようになったAutomatic Static Optimizationが搭載されたことから、このHydrationも一筋縄ではいかなくなっています。

next-redux-wrapperのバージョン6では、Hydration時には HYDRATE ActionがDispatchされるようになっています。SSR時に生成された状態がWebブラウザへ渡されるときや、Webブラウザで既にStore状態を保有しているケースからSSGで生成されたページ(Store状態あり)へ移動したときなどに、このActionが発火されます。つまり、SSRやSSGで生成されたStore状態を“初期値”としたとき、既に別のStore状態を保有しているケースにそれら初期値を受け取る場合、どうマージするかを明示的に記述しなければいけません。SSRではなくSSGを利用することが推奨される今の環境では、このマージは極めて重要になります。

next-redux-wrapperのHydrationを考慮したReducer
const reducer = (state, action) => {
  switch (action.type) {
    case HYDRATE:
      // `HYDRATE` ActionのPayloadには、初期値となるStore状態が格納されている
      return { ...state, ...action.payload };
    default:
      return combineReducer(state, action);
  }
};

多くの場合 HYDRATE Actionによって渡ってくる初期値のStore状態を現在のStore状態に上書きする形で解決するかと思いますが、SSGなどで生成されたページを開いたときに現在のStore状態が上書きされたくない場合は、この方法をうまく処理してあげなければいけません。例えばページ遷移後も特定のフラグ状態を保持したい場合、闇雲に HYDRATE ActionによるState郡を既存のStoreに上書きしてしまうと、当該フラグの状態がリセットされてしまうことになります。

余談:どのようにしてnext-redux-wrapperはHydrationしているのか

SSRやSSGされたページは、next-redux-wrapperによって、初期値となるStore状態が予めセットされます。この原理はnext-redux-wrapper独自のものではなく、 getServerSideProps などのNext.jsが備えるライフサイクルにおけるPropsのHydrationと同じ仕組みが採用されています。

Next.jsには pageProps と呼ばれる、ページの初期状態を格納するオブジェクトが存在します。 publicRuntimeConfigData Fetching Methodsなどでも利用されるオブジェクトです。Reduxの初期状態を持つSSRもしくはSSGされたページのHTMLファイルを開くと、 __NEXT_DATA__ というIDが付与された script 要素に、Next.jsに関するあらゆるデータの初期値がJavaScriptのオブジェクト(連想配列)で格納されていることが分かります。next-redux-wrapperは、この pagePropsinitialState プロパティにJSONシリアライズされたStore初期状態を格納しているわけです。

SSR/SSGされたページのHTMLファイルのソース(一部)
<script id="__NEXT_DATA__" type="application/json">
{
  "props": {
    "pageProps": {
      "initialState": "{\"foo\":{\"bar\":false}}"
    },
    "__N_SSG": true
  },
  "page": "/en/support",
  "query": {

  },
  "buildId": "development",
  "nextExport": false,
  "isFallback": false,
  "gsp": true
}
</script>

この pageProps は、Next.jsのプライベートAPI isSerializable によってシリアライズ可能かどうかがチェックされます。 getInitialProps などの従来のライフサイクルでHydrationされていたPropsは、すべて無理やり文字列へシリアライズされていましたが、Next.js 9.3以降のライフサイクルでは、デバッグを容易にさせるためにシリアライズにチェックが入るようになっています16。例えば、 undefined が含まれるオブジェクトを getServerSideProps などでPropsとして渡す場合、エラーが生じるのも isSerializable によって undefined 値が拒絶されることが原因です。

/pages/index.jsx
export async function getServerSideProps(context) {
  return {
    props: {
      // `isSerializable` によって `undefined` は返せない!
      // Error: Error serializing `.foo` returned from `getServerSideProps` in "/".
      foo: undefined,
    };
  };
}

next-redux-wrapperによるシリアライズも isSerializable を通して検証が行われるため、虚無の値として null ではなく undefined を扱うようなStateを持つプロジェクトの場合、SSRやSSG時にエラーが生じてしまいます。幸運にもnext-redux-wrapperではStateのシリアライズ方法を変更することができるため、 JSON.serialize JSON.parse などを用いて、強制的に undeifned 値を除去する方法が一番手っ取り早いでしょう17

next-redux-sagaは廃止された

Next.jsでredux-sagaを採用している場合、next-redux-sagaを導入することも多いかと思いますが、今年の夏をもって開発が終了しました

next-redux-wrapperでredux-sagaを利用する
export const getStaticProps = nextReduxWrapper.getStaticProps(async ({ store }) => {
  store.dispatch(resourceGetter.requestToGet.started());

  // 一連の処理を終了するための `END` ActionをDispatchする
  //   -- これをDispatchしないと永遠にレンダリングされない
  store.dispatch(END);
  await store.sagaTask.toPromise();
});

next-redux-wrapperのバージョン6によって、 getServerSideProps または getStaticProps をラップするAPIが用意されており、その中でRedux Storeを参照可能なAsync Functionを利用できるため、next-redux-sagaを利用する必要性がなくなったためです。SSRでもSSGでも、next-redux-wrapperによってページレンダリング前にredux-sagaを利用、そしてTask終了を待つことができます。

Redux Storeに秘匿情報を入れてはいけない

上述のHydrationの方法から分かる通り、Storeに格納された初期状態はHTMLに含まれた状態で配信されるため、Redux Storeに秘匿情報を絶対に入れてはいけません。何らかの認証トークンをHTMLファイルに埋め込むことと同じです。

秘匿情報はCookieのような安全性がある程度確立された機構に格納するか、せめて暗号化された文字列を用いるようにしてください。

13. その他DXを向上させるTips

TypeScriptにおける型のre-exportは export type を使う

Next.js 9からはNext.js自体がTypeScriptで記述されるようになったのもあって、デフォルトでTypeScriptを扱えるようになりました。 /pages 以下に置くコンポーネントのファイルは .js .jsx .ts .tsx のいずれでもうまく動作します。

Next.jsではNext.js 7より、Babel( @babel/preset-typescript )を用いてTypeScriptがトランスパイルされています。別の記事でも述べた通り、Babelによるトランスパイルは型関連のre-export問題を孕むため、 export * 構文を使わざるを得ませんでした。しかし、TypeScript 3.8からは export type 構文がサポートされたため、Babel上でもどれが型でどれが値なのかを判別できるようになっています。

したがって、インデックスファイル等で型をre-exportするような場面に置いては、必ず値の export と型の export type を記述するようにしましょう。前述の通り、TypeScript 3.8以上の環境が必要です。

index.ts
export { Foo } from "./foo";
export type { Props as FooProps } from "./foo";

なお型の取り込み import type もTypeScript 3.8でサポートされていますが、これを積極的に利用するかどうかはプロジェクト次第だと思います。今のところ僕は使い分けずに、値も型も import で統一しています。

TypeScriptのランタイム型チェックを無効化する

Next.jsがTypeScriptに対応したバージョン9からは、開発環境にてランタイムで型チェックが走るようになっています。型チェック自体はfork-ts-checker-webpack-pluginによって別プロセスで行われるので、Next.jsのビルド速度に大きな影響を与えることはありませんが、ちょっとしたデバッグ時に型のエラーがWebブラウザ上にデカデカと表示されては煩わしいので、これを無効にすることをオススメします。

next.config.js
module.exports = {
  typescript: {
    ignoreBuildErrors: true,
  },
};

next.config.js: Ignoring TypeScript Errors | Next.js

なおこの機能の有効化/無効化を問わず、CI等で、本番環境へデプロイする前に必ず型検証を済ませておきましょう。TypeScript付属のtscを用いる場合は noEmit オプションを併用することで簡単に型検証が可能です。

tscで型検証
$ npx tsc --noEmit

ReactのStrict Modeを利用する

Reactには、潜在的な問題点を洗い出すためのStrict Modeと呼ばれるツールが用意されています。一般的には React.StrictMode コンポーネントで検証したいコンポーネントを囲うことで利用できますが、Next.jsではバージョン9.1.7より、 next.config.js のプロパティですべてのページに対して有効にできます。

next.config.js
module.exports = {
  reactStrictMode: true,
};

next.config.js: React Strict Mode | Next.js

デフォルトでは有効になっていないものの、React自体のアップデートに備えるためにも、 true にしておくことをオススメしておきます。一方で、非推奨となったReactの componentWillReceiveProps などといったライフサイクルが利用されている場合、Warningが大量発生する可能性があるので注意してください。React Selectのような一部のライブラリでは、未だに非推奨なライフサイクルに依存しているため、完全に排除するにはまだ時間がかかりそうです。

next-secure-headers を入れて、早めにCSPを有効にする

JavaScriptがWebフロントエンド開発で欠かせない存在になってから、Webサービスに対する脅威は格段に増えました。それとともに、Webブラウザが備えるセキュリティ機能も向上し、今日ではいくつかのレスポンスヘッダを設定することで、Webブラウザに対して適切なセキュリティレベルを伝えることができます。Node.jsの場合はHelmetというライブラリが存在しますが、Next.jsではCustom Serverを使わない限り、このライブラリを使うことができません。

そこでnext-secure-headersというライブラリを開発しました(宣伝)。まさに上述のHelmetのNext.js対応版といった位置付けですが、いくつかレガシーな技術に対するヘッダには対応していません。

このライブラリではCSPを始めとする様々なセキュリティ関連のヘッダをNext.jsへ適用させることができますが、とりわけCSPは現代のWebフロントエンド開発における必須設定項目と言えるほど重要な設定です。XSSはWebフロントエンドにとって最悪の最悪な脆弱性であるため、ホワイトリスト形式でCSPをしっかりと設定しておきましょう。

特に、CSPを始めとするホワイトリスト形式のブロッキング効果のあるヘッダは、プロジェクトの規模が膨らんだあとに導入すると動作確認で泡を吹く可能性が高いので、早めに導入することをオススメします。

CSS Modulesを利用する場合は、必ずビジュアルリグレッションテストを導入する

Next.jsはバージョン9.1から、設定無しでCSS Modulesの利用が可能になりました。 .module.css をファイル末尾につけたCSSファイルは、JavaScriptから import 構文によってインポート可能になったわけです。しかし別の記事でも述べた通り、CSS Modulesには致命的な欠陥が存在するため、ビジュアルリグレッションテストの導入は必須と言えます。

もしCSS Modulesが選択肢に入らないような場合は、styled-componentsのようなCSS in JSを利用するのも手ですし、Vercelが開発したstyled-jsxを利用するのも悪くないでしょう。生のCSSしか触れたことがない人にとっては、どちらも癖のあるライブラリだと思うので、チームメンバーの意向やプロジェクトの特性/規模を考慮して選定してください。なおstyled-componentsをNext.jsで利用する場合は、若干のビルド設定の変更Link コンポーネントの扱い方に注意が必要です。

CIから『Vercel』へデプロイしたい

『Vercel』は、 vercel コマンド一発でデプロイ可能な点が魅力的なサービスですが、事前にCLIからメール通知による認証を終えていないとデプロイすることはできません。認証を終えたあとに、当該プロジェクトの .vercel ディレクトリに認証情報が残り、今後同じ端末からは同じ認証情報でデプロイできるようになります。

一方で、毎回環境がリセットされるCIからデプロイをしたい場合、この認証システムは少し不便です。Vercel CLIは VERCEL_ORG_ID 及び VERCEL_TOKEN という環境変数から認証情報を自動で読み込むことも可能です。これらの情報は .vercel ディレクトリの中身や、『Vercel』アカウントのトークン設定画面から収集し、CIに設定してください。なおVercel CLIの name オプションでデプロイ先プロジェクトを指定することはできますが、Deprecated扱いになっているオプションであるため、 VERCEL_ORG_ID で代替するのが良いでしょう。

そもそも『Vercel』は、GitHub及びGitLabとの連携機能を備えていますが、『GitHub Enterprise』などのセルフホスティングサービスには対応しておらず、また複数のステージング環境を持つ場合や、ユーザによってサブドメイン部分が動的に変わるようなサービスの場合、標準の連携機能ではデプロイをカバーできません。その他にも高度なデプロイを施したい場合に、CIからのデプロイは要件として上がることでしょう。

検索時は -nuxt をつける

Next.jsについてGoogle検索したい場合、 -nuxt をつけましょう。最近では目にすることが少なくなりましたが「もしかして:Nuxt.js」とありがた迷惑なフレーズを何度も目にすることになります。

最後に

冒頭でも述べた通り、僕がNext.jsに触れ始めたNext.js 4の頃は、本当にSSRくらいしか目立った特徴がありませんでした。しかしVercelは、一貫してWebフロントエンドにおけるパフォーマンスとDXを重視してきました。その結果がNext.jsが備えるビルド設定だったり、『Vercel』と併用することによるISRや画像最適化として現れています。

Nuxt.jsとは異なり、Next.jsはVercelが保有するプロジェクトです。OSSという形態ではありますが、実質Vercelユーザを増やすためのプロジェクトで、現在も今後も、真の恩恵を受けるには『Vercel』と併用する必要があります。AWSやGoogleのような大手企業ならまだしも、Webフロントエンドエンジニア以外にはあまり認知されていない一企業に依存する点では、業務に導入しづらい環境もあるかもしれません。
しかしNext.jsの開発には、Google社員やReact開発チームも少なからず参加していること、Vercel社員がGoogle IOなどの大規模なカンファレンスに度々登壇していること、そして今年2020年にはNext.js Confと呼ばれるNext.jsに関するカンファレンスが行われたことを見れば、Next.jsの勢いは衰えるどころか今まさに急成長/急拡大している状態です。更にVercelは、今年の春に2100万ドルの資金を調達しており、多くの投資家から注目を集めている企業でもあります18

Next.js 9は今までのバージョンとはまったく異なるスピードで新機能が搭載され、更に『Vercel』での利用を実質前提としたフレームワークになりました。Next.js 10はまだリリースされたばかりではありますが、一発目でオンデマンドによる画像最適化機能を搭載するなど、革新的な機能が追加されています。今後もReactプロジェクトの幅を広げるためにも、そしてWebフロントエンドの幅を広げるためにも、動向をチェックしつつ何かしらの機会で貢献したいものです。

おひたしおひたし。

P.S. Next.jsやRuby on Rails、AWSが好きな人はTwitterの方もフォローしてね!!!!


【2020/11/06】追記
タイトルが少し煽り気味になっていますが、記事内容を読めば分かる通り、以下の3点を改めて強調しておきます。

  • Next.jsという世界においてのSSRは新しいものではない(≠Webフロントエンド全体におけるSSR)
  • Next.jsという世界においてのSSRは推奨されていない(≠非推奨)が、SSGは推奨されている
  • 特殊な要件/環境下ではSSRも選択肢になりうるので、SSRが絶対使ってはいけない機能ではない
    • 2章の見出しで「使うな」と断言しつつ、本文では「やむなし」と矛盾していたため、こちらは修正いたしました🙇🏻‍♂️

そして記事中では、筆者の考察として「Next.jsは、今後もVercel依存前提のフレームワークとして開発されるのではないか」という内容を記述していますが、海外でも同様の疑念を抱いた方がいるらしく、それに対してNext.jsのコアメンバーである@timneutkens氏がTwitter上で反論しているとのことです。

@timneutkens 氏の発言では、例えとして、(本記事中でも述べたような)画像最適化時のホスティングサービスをVercel以外でも利用できるといった旨が紹介されています。ただ個人的な意見としては、ISRなどの主要機能を利用する場合Vercel以外の選択肢がないため、少なくとも現時点ではVercel依存から逃れられないという点に変わりはなく、Next.jsのISRに対応した代替サービスもしばらく登場しないのではないかと思っています。

(情報提供、ありがとうございます:@_thesugar_氏)

【2020/11/07】追記
3章内で「Next.jsのAPI RoutingまたはSSRされたページは1サーバレスファンクションが必要となり、プランによるサーバレスファンクション数制限に引っかかるため、SSRはなおさらオススメできない」という内容を記述していました。これは私が今年の夏、サーバレスファンクション数の制限が24となるProプラン上で、25ページ以上SSRしているNext.jsプロジェクトをデプロイした際に、上限到達のエラーに面した……という経験に基づくものでした。しかし現在では上限を超過したNext.jsプロジェクトのデプロイは成功するようで、これについて改めてVercelに問い合わせてみたところ「数ヶ月前に仕様変更が入り、今ではデプロイが成功する」といった回答をいただいたため、当該内容を削除しました。

なお回答では「今ではデプロイが成功する」といった文言であったため、Next.jsではサーバレスファンクション数の制限が免除されるのか、もしくは使用状況に応じて制限が課されるのかは不明です。また、Vercelの公式ドキュメントでは以下のように記述されています。

NOTE: Next.js bundles all API Routes and server-rendered pages into individual Serverless Functions when deployed on Vercel. As a result, the "Serverless Functions per Deployment" limit is unlikely to apply for Next.js projects.

(情報提供、ありがとうございます:@sankentou氏)


  1. README.md 自体は存在しますが、Next.js公式サイトのドキュメントと README.md のドキュメントの二重管理になっていたため README.md からドキュメント内容がごっそり削除されました。 

  2. Blog | Next.js より引用。 

  3. /users/:user_id のような任意のパラメータを受け取る形式のルーティングを指します。外部ライブラリを利用することで対応できましたが、当時は標準対応していませんでした。 

  4. Googleによる「Rendering on the Web - Web上のレンダリング | Google Developers」における“Static SSR”と同義です。 

  5. 方法は3通りあります。1つ目は getServerSideProps などのライフサイクル関数の引数から取得する方法、2つ目は next/link パッケージの useRouter によるHooks APIを利用する方法、3つ目は next/link パッケージの withRouter によるHOCを利用する方法です。詳細は公式ドキュメントを参照してください。 

  6. next/link パッケージの useRouter によるHooks APIを利用する方法の他に、 next/link パッケージの withRouter によるHOCを利用する方法があります。 

  7. 追加されたコンテンツのHTMLの静的生成は行われないので、SEOしたければ再ビルドが必要となります。 

  8. VercelもStale While RevalidateにインスパイアされてIncremental Static Regenerationを開発したと述べており、また同社はswrというフェッチライブラリを公開しています。 

  9. ここでは、認証後のコンテンツを含んだHTMLファイルの生成を意味しています。 

  10. Fetch APIのPolyfillは、Webブラウザ向けがNext.js 9.1.7で、Node.js向けがNext.js 9.4で搭載されました。 

  11. 日本人Vercel社員である@chibicode氏によると「バーセル」が英語に近い発音となるようです。 

  12. 公式ドキュメントで言及されている他、問い合わせ時に「AWS及びGCPを利用している」と回答されました。 

  13. loader として読み込み先を指定できるようになっています。 

  14. SSGでは、Custom Serverによって導入したプラグイン等の恩恵を受けることはできません。 

  15. 公式ブログでは「Next.js 10.0から不要」というような文脈で紹介されています。 

  16. getServerSideProps cannot be serialized as JSON. Please only return JSON serializable data types · Issue #11993 · vercel/next.js 

  17. 本来は、Immutable.jsなどのJSON化しづらいオブジェクトをシリアライズするための機能です。 

  18. ちょうどこのタイミングでZEITからVercelへ社名が変更され、『Now』から『Vercel』へサービス名が変更されました。 

jagaapple
こんにちは、よろしくおねがいします。TypeScript/React/Next.js/Swift/Ruby/Ruby on Railsが好きです。 Webデザイン不定期/Webフロント16年/サーバサイド8年/iOS2年
ユーザー登録して、Qiitaをもっと便利に使ってみませんか。
  1. あなたにマッチした記事をお届けします
    ユーザーやタグをフォローすることで、あなたが興味を持つ技術分野の情報をまとめてキャッチアップできます
  2. 便利な情報をあとで効率的に読み返せます
    気に入った記事を「ストック」することで、あとからすぐに検索できます
  • 3. Next.jsは、『Vercel』でのホスティング前提のフレームワークだと認識する
  • 4. Next.jsでAPIサーバを建てない/『Vercel』でAPIサーバを建てない
  • 5. 『Vercel』を使うなら next dev ではなく vercel dev を用いる
  • 6. Link で正しくリンクを貼る方法を知る
  • 実際に生成されるHTMLの a 要素に href 属性が設定されているか確認する
  • 余談:Next.jsのページ遷移の仕組み
  • 7. webpackの設定には極力触れない、Zero Configをできる限り保つ
  • 8. バージョンアップ時のZero Breaking Changesを鵜呑みにしない
  • 9. ページコンポーネントには、ロジックを書かない
  • 10. meta 要素には key Propをつける
  • 11. ルーティングとリンク先を一元管理するファイルを作成する
  • 12. Reduxを用いる場合は、Hydrationのマージ方法に留意する
  • 13. その他DXを向上させるTips
  • 最後に
  • この記事は以下の記事からリンクされています
    コメント

    このあたりに疎くて申し訳ありません。以下のような話をしている、で正しいですか。

    Single Page Applicationを前提に考えた時、サイトを開くためには最初にhtml/css/jsをクライアントがロードする必要が有りますが、単純なやり方として

    1, Client side rendering - サーバーが単純に必要なhtml/css/jsを返し、クライアントのブラウザ側でReactを読み込み、このReactが各コンポーネントのレンダリングをする

    があり、しかしこれだとクライアント(ユーザー)視点で最初のロードが長いので

    2, Server side rendering - あらかじめサーバーにReactをおいておき、サーバーがクライアントからリクエストを受けたときにサーバー側でReactがhtml/cssを生成してから生成後のhtml/cssをクライアントに返す(Reactをクライアント側で実行する必要がない)

    という手法が考えられますが、SSRはもう古く、

    3, Static Site Generation (SSG) - React等による動的なhtml/css生成をランタイム時に(サーバー/クライアント双方で)しない、かわりに開発者がコードのコンパイル時にそのウェブサイトに必要なすべてのhtml/cssを生成してしまう。これをサーバーにデプロイして、ユーザーはサーバーから静的なhtml/cssを読み込む。

    がモダンなやり方。で正しいでしょうか。

    ファイルアップロードが可能なサービスの場合は、簡単に帯域制限の上限に到達してしまう

    Amazon S3やGoogle Cloud Storageなどではクライアント側から直接アップロードすることができます
    Vercel自体はストレージを持たず、どのみち外部にあるサーバーにアップロードすることになってしまうのでファイルアップロードでVercelを経由させることは回避できるのではないでしょうか

    SSRをしている場合、1ページ1ファンクションを必要とするため、12ページまでしかデプロイできないことになります。

    Next.js内で作成したAPIのファイルは、1ファイル1ファンクションとしてデプロイされます。

    とありますが、試しに API Route 13個以上 かつ SSRしているページ 13個以上
    のプロジェクトを作ってVercelの無料プランにデプロイしてみたところ、問題なくデプロイすることができました。

    もしかすると、Next.jsを使っている場合はこの制限に引っかからないかもしれないので、
    よければ一度手元でも試してみてもらえると嬉しいです。

    @lechatthecat 仰る通りです。補足をすると、「Next.jsにおけるSSRは古く、Next.jsにおける新しいWebページの実装/デプロイ方法がSSG」となります。Nuxt.jsなど他フレームワークやプラットフォームではまた状況が異なるかと思います。

    @yuta0801 仰る通りです。その場合、ファイルアップロードとアップロードされたファイル情報をデータベースへ書き込む処理が必要で、俯瞰的に見るとトランザクションの管理が必要になります(これについて本文中に追記しました)。

    @sankentou おお、ありがとうございます!今夏にSSRを全面的に採用していたプロジェクトで上限問題に面したため本文中にその旨を記述していましたが、コメントを頂いてから新規プロジェクトで検証し、かつVercelへ問い合わせてみたところ現在ではデプロイが成功するとのことでした。これについて本文中に追記し、当該内容を削除いたしました。

    (編集済み)

    next build && next startで一定時間キャッシュするというISR自体の挙動が確認できたのでセルフホストすることは可能だと思いますが、記事中に

    ISRに対応したプラットフォームは『Vercel』しか存在しません
    ISRなどの主要機能を利用する場合Vercel以外の選択肢がない

    と書かれているのは、Vercel側に実装されているホスティングの最適化を実施しないとISRの効果が最大化できないという理解であっていますか?

    素晴らしい記事をありがとうございます。

    ビルドした内容には有効期限を設けることができ、有効期限を過ぎたページにアクセスされた場合は、前回ビルドされたコンテンツを返しつつも、バックグラウンドで再ビルドされることになります。

    ISR使ってみたいのですが、この場合ページ編集機能をリアルタイムに反映するのは難しいでしょうか?
    例えばrevalidateに60sを設定して、ページ編集に30秒かかった場合、元のページをリロードしても最大30秒+再ビルド時間が経過するまでは元のページが返却される認識で合いますでしょうか?

    あなたもコメントしてみませんか :)
    すでにアカウントを持っている方は
    ユーザーは見つかりませんでした