# SSG

Static-Site Generation(SSG)とは、あらかじめビルド時にコンテンツを生成してから配信する方法です。 SSRでは、クライアントからのリクエストがあったタイミングでサーバ上でレンダリングを実行して、コンテンツを生成します。

SSRでレンダリング

一方、SSGは、アプリケーションをビルドするタイミングで同時にコンテンツを生成します。 ビルド時にコンテンツを生成するため、クライアントからサーバへリクエストがあったときには、既にコンテンツが準備してある状態になります。 そのため、サーバ上ではレンダリングをする必要がなく、生成済みのHTMLを配信するだけになります。

ビルドのタイミングでコンテンツを生成する

サーバ上でレンダリングする必要がないと、即座にクライアントへコンテンツを配信することができます。 そのため、SSGの方がよりパフォーマンス性に優れていると言えます。また、CDNへのキャッシュのしやすさなどの利点もあります。 SSGの実装は、Next.jsやNuxt、Gatsby、VuePressなどがサポートしています。

# SSGの仕組み

では、SSGの仕組みをNext.jsを例に見てみましょう。 コーポレイトサイトのようなWebページを想定してみましょう。 SSGが実行されて、クライアントへ配信されるまでのステップは、次のようになります。

ビルド時に実行

  1. アプリケーションをビルドする (next build)
  2. ビルド時にHTMLファイルが生成される

SSG-1-2.jpg

サーバ側で実行

  1. ブラウザがサーバへリクエストをする
  2. サーバはリクエストを受け取り、生成されたコンテンツ(HTML)をブラウザ(クライアント)に返す

SSG-1-3.jpg

クライアント側で実行

  1. ブラウザ側でHTMLファイルを解析し、JavaScriptファイルをダウンロードする
  2. JavaScriptファイルを実行しReactを起動する
  3. すでに生成されたコンテンツ(HTML)に対して、ハイドレーションを実行する
  4. ハイドレーション後、アプリケーションが機能する状態になる

SSG-1-4.jpg

はじめに、Next.jsのSSGではビルド時にHTMLファイルが生成されます。

例えば、以下のようなAboutページがあるとしましょう。

// pages/about.tsx

export default function About() {
  return <div>
    <h1>Frontend Design会社について</h1>
    {/* ... */}
  </div>
}

このページをnext buildでビルドします。

// next buildが実行される
$ yarn build

すると、about.htmlというHTMLが生成されます。

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="utf-8" />
        <meta name="viewport" content="width=device-width" />
        <meta name="next-head-count" content="2" />
        <link rel="preload" href="/_next/static/css/876d048b5dab7c28.css" as="style" />
        <link rel="stylesheet" href="/_next/static/css/876d048b5dab7c28.css" data-n-g="" />
        <noscript data-n-css=""></noscript>
        <script defer="" nomodule="" src="/_next/static/chunks/polyfills-c67a75d1b6f99dc8.js"></script>
        <script src="/_next/static/chunks/webpack-8fa1640cc84ba8fe.js" defer=""></script>
        <script src="/_next/static/chunks/framework-2c79e2a64abdb08b.js" defer=""></script>
        <script src="/_next/static/chunks/main-74c4d6b2b5c362f3.js" defer=""></script>
        <script src="/_next/static/chunks/pages/_app-cff197596e2d3fb7.js" defer=""></script>
        <script src="/_next/static/chunks/pages/about-c9c253072c621717.js" defer=""></script>
        <script src="/_next/static/LZ95_3iccW5qecKk2uZPx/_buildManifest.js" defer=""></script>
        <script src="/_next/static/LZ95_3iccW5qecKk2uZPx/_ssgManifest.js" defer=""></script>
    </head>
    <body>
        <div id="__next">
            <div><h1>Frontend Design会社について</h1></div>
        </div>
        <script id="__NEXT_DATA__" type="application/json">
            { "props": { "pageProps": {} }, "page": "/about", "query": {}, "buildId": "LZ95_3iccW5qecKk2uZPx", "nextExport": true, "autoExport": true, "isFallback": false, "scriptLoader": [] }
        </script>
    </body>
</html>

この時点で以下の二つのステップが完了します。

  1. アプリケーションをビルドする (next build)
  2. ビルド時にHTMLファイルが生成される

生成されたHTMLを見ると、JavaScriptファイルがいくつか定義されているのが分かります。これは、ブラウザー側でハイドレーションをするためのファイルになります。 SSGでも、CSRやSSRと同様に、ブラウザー側でReactを起動します。そして、DOMにイベントをバイディングして、アプリケーションが機能できるように処理をします。










 
 
 
 
 
 
 
 











<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="utf-8" />
        <meta name="viewport" content="width=device-width" />
        <meta name="next-head-count" content="2" />
        <link rel="preload" href="/_next/static/css/876d048b5dab7c28.css" as="style" />
        <link rel="stylesheet" href="/_next/static/css/876d048b5dab7c28.css" data-n-g="" />
        <noscript data-n-css=""></noscript>
        <script defer="" nomodule="" src="/_next/static/chunks/polyfills-c67a75d1b6f99dc8.js"></script>
        <script src="/_next/static/chunks/webpack-8fa1640cc84ba8fe.js" defer=""></script>
        <script src="/_next/static/chunks/framework-2c79e2a64abdb08b.js" defer=""></script>
        <script src="/_next/static/chunks/main-74c4d6b2b5c362f3.js" defer=""></script>
        <script src="/_next/static/chunks/pages/_app-cff197596e2d3fb7.js" defer=""></script>
        <script src="/_next/static/chunks/pages/about-c9c253072c621717.js" defer=""></script>
        <script src="/_next/static/LZ95_3iccW5qecKk2uZPx/_buildManifest.js" defer=""></script>
        <script src="/_next/static/LZ95_3iccW5qecKk2uZPx/_ssgManifest.js" defer=""></script>
    </head>
    <body>
        <div id="__next">
            <div><h1>Frontend Design会社について</h1></div>
        </div>
        <script id="__NEXT_DATA__" type="application/json">
            { "props": { "pageProps": {} }, "page": "/about", "query": {}, "buildId": "LZ95_3iccW5qecKk2uZPx", "nextExport": true, "autoExport": true, "isFallback": false, "scriptLoader": [] }
        </script>
    </body>
</html>

次に、next startでアプリケーションを起動してみましょう。

// next startを実行
$ yarn start

ready - started server on 0.0.0.0:3000, url: http://localhost:3000

http://localhost:3000/about にアクセスすると上記で生成されたAboutページが表示されます。

SSG-1.png

このときに既にHTMLファイルは生成されているので、サーバ側の処理はHTMLファイルを配信するだけになります。

クライアント側では、JavaScriptファイルをダウンロードして、Reactを起動します。 そして、Reactはハイドレーションを実行して、アプリケーションが機能する状態になります。

今の状態だとHTMLが表示されるだけなので、ボタンを追加してみましょう。


 
 
 



 




export default function About() {
  const handleClick = () => {
    window.alert('ボタンを押しました!')
  }

  return <div>
    <h1>Frontend Design会社について</h1>
    <button onClick={handleClick}>ボタン</button>
    {/* ... */}
  </div>
}

再度ビルドして、http://localhost:3000/about にアクセスするとボタンが動作しているのが分かります。

SSG-2.gif

# 動的コンテンツ

Next.jsは、SSGの動的コンテンツにも対応しています。 例えば、ブログの記事一覧ページを想定してみましょう。

ブログの記事一覧ページでは、ブログの記事データを取得する必要があります。 その場合、Next.jsでは getStaticPropsを使うことで実現できます。

getStaticPropsはビルド時に実行され、外部APIへのアクセスなどが可能となっています。

// pages/articles/index.tsx

export async function getStaticProps() {
  return {
    props: {
      // 外部APIへ非同期でデータを取得する
      articles: await getArticlesFromDatabase()
    }
  }
}

// APIから取得したデータを元にコンテンツを生成する
export default function Articles({ articles }) {
  return (
    <>
      <h1>ブログ一覧</h1>
      <ul>
        {articles.map((article) => (
          <li key={article.id}>{article.name}</li>
        ))}
      </ul>
    </>
  )
}

このようにgetStaticPropsを使うことによってビルド時に動的コンテンツの生成が可能となります。

# 動的パス

getStaticPropsでは、一覧ページのコンテンツを生成することができました。

getStaticPathsを使うとブログの個別ページも生成することができます。

例えば、/articles/1/articles/2、... /articles/100のように個別のページをビルド時に生成します。

// pages/articles/[id].tsx

export async function getStaticPaths() {
  // 外部APIからブログ記事のデータを取得
  const articles = await getArticlesFromDatabase()

  // `/articles/1`、`/articles/2`、... `/articles/100`のパスを生成するためのIDを整形する
  const paths = articles.map((article) => ({
    params: { id: article.id }
  }))

  return { paths, fallback: false }
}

export async function getStaticProps({ params }) {
  return {
    props: {
      // 該当のIDをもとに個別ページのデータを取得する
      article: await getArticleFromDatabase(params.id)
    }
  }
}

// APIから取得したデータを元にコンテンツを生成する
export default function Article({ article }) {
  return (
    <>
      <h1>{article.name}</h1>
    </>
  )
}

仮にブログ記事が100個あった場合、Next.jsは100個のHTMLをビルド時に生成します。

試しにnext buildをしてみましょう。

$ yarn build

すると、.next/server/pages/articlesに以下のようにファイルが生成されているのが分かります。

tree .next/server/pages/articles/                                                                                                                                                            (git)-[main]
.next/server/pages/articles/
├── 1.html
├── 1.json
├── 10.html
├── 10.json
├── 11.html
├── 11.json
├── 12.html
├── 12.json
├── 13.html
...
├── 100.html
├── 100.json

# メリット

# TTFB、FCP、TTIの向上

SSGは事前にコンテンツを生成することで、サーバでのレンダリングを省略することができます。 そのため、サーバからのレスポンスが高速になり、かつキャッシュも効きやすいため、クライアントがサーバへ問い合わせてから画面が描画されるまでの時間が大幅に短縮されます。 その結果、FCPとTTIのパフォーマンスも大きく向上します。

FCPとTTIのパフォーマンスの向上

# SEOの改善

サーバからのレスポンスが高速になり、アプリケーション起動までのパフォーマンスが向上することで、クローラーボットは素早くインデックスをすることができます。 SSRと比べて、レンダリングする処理がないため、よりSEOに有効になります。

# デメリット

# ビルド・デプロイに時間がかかる

動的パスで紹介した例のように、動的コンテンツを生成する場合は、事前にAPIへ問い合わせをしてビルドする必要があります。 100ページほどのコンテンツであればそれほど時間を要しないかもしれません。しかし、100万ページにもなれば、ビルドにかなりの時間がかかってしまう可能性があります。

また、一つのコンテンツを変更するたびに再ビルドが必要になると、デプロイするまでのコストが高くなり、変更を適用してからリリースまでのリードタイムが長くなる可能性があります。 そのため、リアルタイムにデータを反映させることが難しくなるでしょう。

# SSGに適したサービス

上記のようなメリット・デメリットを考慮すると、リアルタイムで更新しなくてもいいサービスはSSGに向いています。 例えば、更新性が低く静的コンテンツが多いコーポレイトサイトや記事数が少なめのブログやメディアサイトなどが当てはまります。