# SSG
Static-Site Generation(SSG)とは、あらかじめビルド時にコンテンツを生成してから配信する方法です。 SSRでは、クライアントからのリクエストがあったタイミングでサーバ上でレンダリングを実行して、コンテンツを生成します。
一方、SSGは、アプリケーションをビルドするタイミングで同時にコンテンツを生成します。 ビルド時にコンテンツを生成するため、クライアントからサーバへリクエストがあったときには、既にコンテンツが準備してある状態になります。 そのため、サーバ上ではレンダリングをする必要がなく、生成済みのHTMLを配信するだけになります。
サーバ上でレンダリングする必要がないと、即座にクライアントへコンテンツを配信することができます。 そのため、SSGの方がよりパフォーマンス性に優れていると言えます。また、CDNへのキャッシュのしやすさなどの利点もあります。 SSGの実装は、Next.jsやNuxt、Gatsby、VuePressなどがサポートしています。
# SSGの仕組み
では、SSGの仕組みをNext.jsを例に見てみましょう。 コーポレイトサイトのようなWebページを想定してみましょう。 SSGが実行されて、クライアントへ配信されるまでのステップは、次のようになります。
ビルド時に実行
- アプリケーションをビルドする (
next build
) - ビルド時にHTMLファイルが生成される
サーバ側で実行
- ブラウザがサーバへリクエストをする
- サーバはリクエストを受け取り、生成されたコンテンツ(HTML)をブラウザ(クライアント)に返す
クライアント側で実行
- ブラウザ側でHTMLファイルを解析し、JavaScriptファイルをダウンロードする
- JavaScriptファイルを実行しReactを起動する
- すでに生成されたコンテンツ(HTML)に対して、ハイドレーションを実行する
- ハイドレーション後、アプリケーションが機能する状態になる
はじめに、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>
この時点で以下の二つのステップが完了します。
- アプリケーションをビルドする (
next build
) - ビルド時に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ページが表示されます。
このときに既に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
にアクセスするとボタンが動作しているのが分かります。
# 動的コンテンツ
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のパフォーマンスも大きく向上します。
# SEOの改善
サーバからのレスポンスが高速になり、アプリケーション起動までのパフォーマンスが向上することで、クローラーボットは素早くインデックスをすることができます。 SSRと比べて、レンダリングする処理がないため、よりSEOに有効になります。
# デメリット
# ビルド・デプロイに時間がかかる
動的パスで紹介した例のように、動的コンテンツを生成する場合は、事前にAPIへ問い合わせをしてビルドする必要があります。 100ページほどのコンテンツであればそれほど時間を要しないかもしれません。しかし、100万ページにもなれば、ビルドにかなりの時間がかかってしまう可能性があります。
また、一つのコンテンツを変更するたびに再ビルドが必要になると、デプロイするまでのコストが高くなり、変更を適用してからリリースまでのリードタイムが長くなる可能性があります。 そのため、リアルタイムにデータを反映させることが難しくなるでしょう。
# SSGに適したサービス
上記のようなメリット・デメリットを考慮すると、リアルタイムで更新しなくてもいいサービスはSSGに向いています。 例えば、更新性が低く静的コンテンツが多いコーポレイトサイトや記事数が少なめのブログやメディアサイトなどが当てはまります。