公式チュートリアルでNext.jsに入門してみた (2) 〜Pre-rendering、データフェッチ〜

2022.01.03

こんにちは、CX事業本部 IoT事業部の若槻です。

フロントエンドフレームワークNext.jsへの入門のために、次の公式チュートリアルを数回のシリーズに分けてこなしていき、基本的な機能に触れていこうと思います。

やってみた

本記事(2)では、チュートリアルのうち「Pre-rendering and Data Fetching」をやっていき、Next.jsのPre-renderingおよびデータフェッチについて理解を深めていきます。

Pre-rendering and Data Fetching

Pre-rendering

デフォルトでは、Next.jsではすべてのページのPre-renderingが有効となっています。これによりNext.jsはPre-renderingにより各ページのHTMLを事前にサーバーサイドで生成するため、クライアントサイド(ブラウザ)ですべてのJavaScript実行を行うのに比べて、パフォーマンスやSEO(Search Engine Optimization)の面で有利となります。

生成されたHTMLはブラウザ上で最低限のJavaScriptを実行し、インタラクティブなページを完成させます。このプロセスをhydration(ハイドレーション)と呼びます。

Check That Pre-rendering Is Happening

Pre-renderingの動作を試してみます。

ブラウザのJavaScriptを無効にします。

Next.jsで構築されたWebサイトhttps://next-learn-starter.vercel.app/にアクセスすると、JavaScriptを実行せずにページを正常にレンダリングできています!

これはNext.jsがページコンテンツを静的なHTMLに変換しているからです。

Summary: Pre-rendering vs No Pre-rendering

下記は、Pre-rendering(Next.jsを使用)と、非Pre-rendering(Plain Reactを使用)の違いの概要についてです。

Pre-renderingでは、初回のHTMLのロードが素早く行われるためページの表示がすぐに行われます。その後<Link >などのJavaScriptの実行が必要な一部要素の処理が遅延して行われます。

一方で非Pre-renderingでは、すべてのReact ComponentがJavaScript実行を待って表示されます。

https://nextjs.org/learn/basics/data-fetching/pre-rendering

Two Forms of Pre-rendering

そしてNext.jsでは2種類のPre-rendering方式をサポートしています。

  • Static Generation is the pre-rendering method that generates the HTML at build time. The pre-rendered HTML is then reused on each request.
  • Server-side Rendering is the pre-rendering method that generates the HTML on each request.

1つはStatic Generation(SSG)で、これはアプリケーションのHTMLの生成が1度だけ行われ、それがページリクエストのたびに再利用されます。

もう1つはServer-side Rendering(SSR)で、こちらはページリクエストのたびにHTMLが再生成されます。

https://nextjs.org/learn/basics/data-fetching/two-forms

Per-page Basis

Next.jsでは同じアプリケーション内でページごとにPre-rendering方式の選択が可能です。そのためSSGとSSRのハイブリッドな構成のアプリケーションも実装できます。

When to Use Static Generation v.s. Server-side Rendering

Next.jsでは、パフォーマンス上の理由で基本的にはSSGの使用が推奨されています。

We recommend using Static Generation (with and without data) whenever possible because your page can be built once and served by CDN, which makes it much faster than having a server render the page on every request.

これはページがデータを含むかどうかに関わらずです。例えば次のようなページではSSGを使用することになります。

  • マーケティングページ
  • ブログ一覧
  • ECサイト商品一覧
  • 参考ドキュメント

一方で、次のようなデータの更新が頻繁に行われるページでは、SSRを使用して表示されるデータを常に最新に保つようにします。(もしくはPre-renderingではなくclient-side JavaScriptを使用します。)

  • 動画サイト(リコメンデーションされる動画がリクエストごとに頻繁に変わる)
  • SNS(常に最新の投稿をトップに表示するようにする)

Static Generation with and without Data

Static Generation with Data using getStaticProps

外部データを取得する必要があるページでもStatic Generation(SSG)は利用可能です。その場合はgetStaticPropsというasync関数を使用します。

Blog Data

それでは実際にNext.jsアプリがブログデータをフェッチする動作を実装してみます。

プロジェクトトップにpostsディレクトリを作成します。

posts配下に、次のブログコンテンツ(マークダウン)をpre-rendering.mdファイルに作成します。

posts配下に、次のブログコンテンツ(マークダウン)をssg-ssr.mdファイルに作成します。

(エディターがマークダウンを誤認識するので画像で載せています。元データはこちら

Implement getStaticProps

マークダウンをパースするためにgray-matterをインストールします。

$ npm install gray-matter

プロジェクトトップにlibディレクトリを作成します。

lib配下に、次のファイルposts.jsを作成します。今回はここで作成するgetSortedPostsDataを外部データをフェッチするライブラリとします。

lib/posts.js

import fs from 'fs'
import path from 'path'
import matter from 'gray-matter'

const postsDirectory = path.join(process.cwd(), 'posts')

export function getSortedPostsData() {
  // Get file names under /posts
  const fileNames = fs.readdirSync(postsDirectory)
  const allPostsData = fileNames.map(fileName => {
    // Remove ".md" from file name to get id
    const id = fileName.replace(/\.md$/, '')

    // Read markdown file as string
    const fullPath = path.join(postsDirectory, fileName)
    const fileContents = fs.readFileSync(fullPath, 'utf8')

    // Use gray-matter to parse the post metadata section
    const matterResult = matter(fileContents)

    // Combine the data with the id
    return {
      id,
      ...matterResult.data
    }
  })
  // Sort posts by date
  return allPostsData.sort(({ date: a }, { date: b }) => {
    if (a < b) {
      return 1
    } else if (a > b) {
      return -1
    } else {
      return 0
    }
  })
}

pages/index.jsを次のように更新します。getStaticPropsを使用してgetSortedPostsDataからデータをフェッチして処理しています。

pages/index.js

import Layout from '../components/layout'
import utilStyles from '../styles/utils.module.css'
import { getSortedPostsData } from '../lib/posts'

export async function getStaticProps() {
  const allPostsData = getSortedPostsData()
  return {
    props: {
      allPostsData
    }
  }
}

export default function Home({ allPostsData }) {
  return (
    <Layout home>
      {/* Keep the existing code here */}

      {/* Add this <section> tag below the existing <section> tag */}
      <section className={`${utilStyles.headingMd} ${utilStyles.padding1px}`}>
        <h2 className={utilStyles.headingLg}>Blog</h2>
        <ul className={utilStyles.list}>
          {allPostsData.map(({ id, date, title }) => (
            <li className={utilStyles.listItem} key={id}>
              {title}
              <br />
              {id}
              <br />
              {date}
            </li>
          ))}
        </ul>
      </section>
    </Layout>
  )
}

しかしここで開発サーバーを起動しようとすると次のエラーとなりました。nextコマンドがどこかに行ってしまったようです。

$  npm run dev

> @ dev /Users/wakatsuki.ryuta/projects/cm-rwakatsuki/nextjs-tutorial/nextjs-blog
> next dev

sh: next: command not found
npm ERR! code ELIFECYCLE
npm ERR! syscall spawn
npm ERR! file sh
npm ERR! errno ENOENT
npm ERR! @ dev: `next dev`
npm ERR! spawn ENOENT
npm ERR! 
npm ERR! Failed at the @ dev script.
npm ERR! This is probably not a problem with npm. There is likely additional logging output above.

npm ERR! A complete log of this run can be found in:
npm ERR!     /Users/wakatsuki.ryuta/.npm/_logs/2022-01-03T15_01_41_996Z-debug.log

本エラーは次のブログが参考になりました。

どうやらgray-matterをインストールした時に何かしらの影響があったようです。npm upgradeしたら開発サーバーを正常に起動できるようになりました。

すると外部データのフェッチが行われ、Pre-renderingによりブログのインデックスが表示できました!

getStaticProps Details

Development vs. Production

今回使用したgetStaticPropsですが、development環境では(おそらくデバッグを容易にするために)リクエスト毎に実行されますが、production環境ではビルド時(HTML生成時)にのみ実行されます。

  • In development (npm run dev or yarn dev), getStaticProps runs on every request.
  • In production, getStaticProps runs at build time. However, this behavior can be enhanced using the fallback key returned by getStaticPaths

つまり、getStaticPropsでは原則として、リクエスト毎にクエリパラメータやHTTPヘッダーを指定して、データの動的取得は行えません。よって欲しいデータのみフィルターするなどしてデータフェッチを効率化することができないです。

Fetching Data at Request Time

Using getServerSideProps

そこでgetStaticPropsの代わりに、getServerSidePropsを使用することで、ビルド時ではなくリクエスト毎に動的なデータのフェッチが可能となります。

これによりTime to first byte (TTFB)は若干遅くなりますが、フェッチ結果がCDNなどにキャッシュされなくなります。

今回は実装は試しませんが、覚えておくと使う機会はあるかと思います。

おわりに

公式チュートリアルでNext.jsに入門してみた (2) 〜Pre-rendering、データフェッチ編〜 でした。

次回はこちらです。

参考

以上