こんにちは、エンジニアのみーや(@miiya387)です。

Reactのフレームワーク「Next.js」への入門として、公式チュートリアルを何回かに分けてまとめていきます。
今回は4つ目の pre-rendering and Data Fetching です。
それ以前の章を知りたい方はReactのフレームワーク”Next.js”を触ってみた「アプリ新規作成 / ページ遷移 / スタイル設定」をご覧ください。

  1. Create a Next.js App
  2. Navigate Between Pages
  3. Assets, Metadata, and CSS
  4. Pre-rendering and Data Fetching
  5. Dynamic Routes
  6. API Routes
  7. Deploying Your Next.js App

やってみる

4. Pre-rendering and Data Fetching

pre-rendering

Next.jsではデフォルトですべてのページにおいてpre-renderingが有効になっています。
pre-renderingにより、ページのHTMLを事前に生成するためクライアントサイドですべてのJavaScriptを実行するのに比べて、パフォーマンスもSEOも良くなります。

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

check That Pre-rendering Is Happening

以下の工程でpre-renderingを確認していきます。

  1. ブラウザのJavaScriptを無効にする
  2. その後Next.jsで実装されているこちらのページにアクセス

chromeのJavaScriptを無効にします(手順はこちら▶︎▶︎

その後、公式で指定があったページ https://next-learn-starter.vercel.app/ にアクセスすると、JavaScriptなしでページのレンダリングができていることが確認できました!

これは、Next.jsがページを静的なHTMLとしてpre-renderingしているから、JavaScriptなしでの表示が可能になります。

Next.jsなしで純粋にReactで実装しているページではこうはなりません。
それを確認するために以下のステップでReactのページでJavaScriptを無効にした状態でアクセスするとどうなるのか確認していきます。

  1. JavaScriptが有効な状態でこちらのページにアクセス
  2. その後、JavaScriptを無効にし、再度同じページにアクセス

まずは指定のページにJavaScriptが有効な状態でアクセスします。
Reactのアイコンがくるくるしているページが見れますね。

その後JavaScriptを無効にし再アクセスすると以下のようにJavaScriptを有効にしてくださいと表示され、ページのレンダリングはされません。

これでNext.jsと違い、Reactではページのpre-renderingがされていないことが確認できました。

Summary: Pre-rendering vs No Pre-rendering

以下は公式で紹介されているNext.js(pre-renderingあり)とPlain React(pre-renderingなし)の概要です。
Next.jsでは、初回のHTMLの表示がすぐに行われ、その後<Link>などのJavaScriptの実行が必要な一部要素のみ遅延して行われます。

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

一方でReactでは、初回表示のものはなく、すべてのReact ComponentがJavaScriptの実行を待ってから表示されます。

普段Reactで開発しているときは、初期描画に時間がかかってしまうのはある程度仕方ないと思っていましたが、Next.jsは基本的に全ページでpre-renderingに対応しているのでその問題は解決しそうですね。

Two Forms of Pre-rendering

Next.jsでは以下の2種類のpre-renderingがあります

  1. Static Generation: ビルド時にHTMLを事前生成し、その後は1度生成されたHTMLをリクエストごとに再利用されます
  2. Server-side Rendering: リクエストのたびにHTMLが再生成されます

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

なお、開発モードの環境では、Static Generationであっても、リクエストごとにpre-renderingされます。

Per-page Basis

Next.jsではページごとに使用するpre-rendered方式を選ぶことができ、SSGとSSRのハイブリッド構成が可能になります。
ページの特徴に合わせて選択できるので、とても便利ですね!嬉しいです!

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

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

Next.jsでは基本的にSSGを使うことを推奨しています。
一度ページを生成してCDNの利用が可能になるため、パフォーマンス的に有利になります。
例えば、次のようなページにSSGが使えます。

  • マーケティングページ
  • ブログ投稿
  • ECサイトの商品一覧
  • ヘルプページ

一方で、データの更新が頻繁に行われるページやリクエストごとに表示するコンテンツを切り替えたいページではSSRを使うのが適しています。
SSGと比べるとページ表示速度のパフォーマンスは下がりますが、ページを常に最新の状態にすることができます。
または、pre-renderingをスキップしてクライアントサイドでJavaScriptを利用して頻繁に更新されるデータを表示することもできます。

CMSツールのような入力テキストがリアルタイムでプレビューに反映されるページや、ユーザーの志向性に合わせて表示内容を切り替えたいページではpre-renderingをスキップする方が適していそうですね。

Static Generation with and without Data

SSGはデータの有無に関わらず利用可能です。
これまで作成したページでは、外部データを取得する必要がなくアプリが本番用にビルドされるときに自動的に静的に生成されます。

出展: https://nextjs.org/learn/basics/data-fetching/with-data

しかし、一部のページでは事前にデータを取得しないと、HTMLの生成ができない場合があります。
ビルド時に、画像ファイルを取得したり外部APIをフェッチしたりデータベースにクエリ実行したりなど、、、

その場合もNext.jsはサポートしており、getStaticPropsを使うと事前処理が可能になります。
getStaticPropsはasync関数で、本番ビルド時に実行され、関数内では外部データをフェッチしてページに渡されます。
開発モードでは各リクエストで実行されます。

export default function Home(props) { ... }

export async function getStaticProps() {
  // Get external data from the file system, API, DB, etc.
  const data = ...

  // The value of the `props` key will be
  //  passed to the `Home` component
  return {
    props: ...
  }
}

Blog Data

ここでは実際にブログデータをフェッチして追加していきます。

プロジェクトトップに posts ディレクトリを作成します。
作成したposts配下にブログファイル(マークダウン)を追加し、内容を以下にします。

---
title: 'Two Forms of Pre-rendering'
date: '2020-01-01'
---

Next.js has two forms of pre-rendering: **Static Generation** and **Server-side Rendering**. The difference is in **when** it generates the HTML for a page.

- **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**.

Importantly, Next.js lets you **choose** which pre-rendering form to use for each page. You can create a "hybrid" Next.js app by using Static Generation for most pages and using Server-side Rendering for others.

次に、同様にposts配下にssg-ssr.mdファイルを追加し、内容を以下にします。

---
title: 'When to Use Static Generation v.s. Server-side Rendering'
date: '2020-01-02'
---

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.

You can use Static Generation for many types of pages, including:

- Marketing pages
- Blog posts
- E-commerce product listings
- Help and documentation

You should ask yourself: "Can I pre-render this page **ahead** of a user's request?" If the answer is yes, then you should choose Static Generation.

On the other hand, Static Generation is **not** a good idea if you cannot pre-render a page ahead of a user's request. Maybe your page shows frequently updated data, and the page content changes on every request.

In that case, you can use **Server-Side Rendering**. It will be slower, but the pre-rendered page will always be up-to-date. Or you can skip pre-rendering and use client-side JavaScript to populate data.

Implement getStaticProps

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

npm install gray-matter

プロジェクトトップに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を次のように更新し、getSortedPostsDataを使ってデータフェッチします。
フェッチしたデータを使うようにHomeコンポーネントも一部書き換えます。

import Layout, { siteTitle } 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>
    )
}

更新後、http://localhost:3000/ にアクセスするとフェッチしたブログデータを使用してpre-renderingできました!

getStaticProps Details

Fetch External API or Query Database

今回はファイルシステムからデータをフェッチするパターンを posts.js に実装しましたが、外部APIからデータをフェッチすることや、データベースに直接クエリを実行することもできます。

外部APIからデータをフェッチする場合は posts.js を以下のようにします。

export async function getSortedPostsData() {
  // Instead of the file system,
  // fetch post data from an external API endpoint
  const res = await fetch('..')
  return res.json()
}

クエリを実行する場合は以下のようにします。

import someDatabaseSDK from 'someDatabaseSDK'

const databaseClient = someDatabaseSDK.createClient(...)

export async function getSortedPostsData() {
  // Instead of the file system,
  // fetch post data from a database
  return databaseClient.query('SELECT posts...')
}

getStaticPropsはサーバーサイドのみで実行されるため、JavaScriptのバンドル対象に入りません。
そのため、実行するクエリなどをブラウザに送信することなく実行することができます。

Development vs. Production

先にも記載しましたが、開発モードの場合はリクエストごとにgetStaticPropsが実行されます。
本番では、ビルド時にのみ実行されます。
つまり、getStaticPropsでは、リクエスト毎にクエリパラメータやHTTPヘッダーを指定して、データの動的取得は行えません。

Fetching Data at Request Time

ビルド時ではなくリクエスト時にデータをフェッチする必要がある場合は、 Server-side Renderingを使います。
Server-side Renderingの場合は、getStaticPropsの代わりにgetServerSidePropsを使う必要があります。

Using getServerSideProps

ブログの例では使わないため実装はしませんが、getServerSidePropsを使うことで
リクエストごとのデータフェッチができるようになります。

export async function getServerSideProps(context) {
  return {
    props: {
      // props for your component
    }
  }
}
Client-side Rendering

pre-rendering時に事前取得する必要のないデータは、JavaScriptを使ってクライアントサイドでレンダリングすることもできます。

  • 外部データを必要としないページの部分をSSGでレンダリング
  • ページが読み込まれたら、JavaScriptを使用してクライアントサイドで外部データをフェッチし、残りの部分にデータを入力

出展: https://nextjs.org/learn/basics/data-fetching/request-time

クライアントサイドでデータフェッチする場合は、React HooksライブラリのSWRをおすすめされています。

ページ表示が終わった後に一部広告を表示する場合などに適していそうですね。ただし、途中で表示項目が増えることでのレイアウトシフトによるUX低下やSEOへの影響は注意しないとなので検証してみる必要がありますね。

まとめ

今回はNex.jsの公式チュートリアルをもとにpre-renderingについて触れてみました。

ページの特性に合わせてSSG, SSRを使いわけができることはとても便利ですよね。

SSGを基本としてクライアントサイドで必要な分だけ後から追加表示するのも魅力的でした。

ここまでのソースはgithubに上げたので興味のある方はぜひ一緒にNext.jsと仲良くなっていきましょう!

次回は「Dynamic Routes, API Routes」をやってみます。

Join Us !

ウエディングパークでは、一緒に働く仲間を募集しています!
ご興味ある方は、お気軽にお問合せください(カジュアル面談から可)

採用情報を見る