Nuxt.jsにおけるSPA,SSR,SSGの使い方と挙動

Nuxt.jsは**SSR(Server Side Rendering)**を比較的簡単に実装できるフレームワークとして有名ですが、ご存知の通り**SPA(Single Page Application)**の作成や**SSG(Static Site Generation)**にも使えます。すでに、多くの方がそれぞれの挙動についての記事をあげてくださっているのですが、最近`mode`プロパティがdeprecatedになったり、一部挙動がわかりづらいと思ったので再度まとめようと思います。

なるだけ間違いがないようにはしていますがもし間違いや誤りがあった場合はコメントでご指摘いただけると助かります！！
(Nuxt.js v2.14.7時点での話をします。)

## SSRとSPAの挙動の違い
NuxtはSSRをする場合はかなり便利なツールだと思いますが、SPAとして使う場面も多くあると思います。
SSRとSPAの違いは、SSRはサーバー側でレンダリングを行い、SPAはクライアント側でレンダリングを行います。双方JavaScriptを利用してHTMLを出力するわけですが、それを行う場所が違うということですね。正直、個人的にはその性質から**SPA(Single Page Application)**と言うより、**CSR(Client Side Rendering)**と呼ぶ方が含意がなく、かつSSRとの対称性も取られるので好みです。SPAと呼ぶのが多いのはおそらく、以前`mode`プロパティがSSRのコンフィグで`"universal"`か`"spa"`を入力させていた名残ではないかなと思います。今回は、多くの場所でSPAと呼んでいるので、準じてSPAと呼びます。

一般的に、SPAはJavaScriptによるレンダリングをクライアント側で行うので、その分初期ロードが長くなると言われています。またレンダリングを行うJavaScriptのファイルも送る必要があるため、アプリケーションによってはかなり大きな通信を必要とします。また近年は解消されたと言われていますが、SEOの問題もあります。これは、クラインアント側でレンダリングを行う関係上ビルドされたHTMLファイルには実際のコンテンツが含まれていないため発生しますが、近年のクローラーはJavaScriptを実行できるらしく、問題はないとされています。

一方、SSRはサーバー側でアクセスの度にレンダリングを行い、完成されたHTMLを返します。これによって初期ロードの問題はなくなり、またレンダリング用のJavaScriptも必要なく通信量も減らせます。しかし、サーバー側でレンダリングを行うため、サーバーにNode.jsの実行環境がなければいけません。

Nuxtでどっちのモードを使うかどうかは`nuxt.config.js`に以下のように記述します。

```js:nuxt.config.js
export default {
    ssr: true, // SSRする場合
    ...
}
```

```js:nuxt.config.js
export default {
    ssr: false, // SPAの場合
    ...
}
```
`mode`プロパティはv2.14.5でdeprecatedになりました。代わりにこの`ssr`プロパティを使いましょう。
詳しい説明はこちらを。[The ssr Property](https://nuxtjs.org/docs/2.x/configuration-glossary/configuration-ssr)

またv2.13.0から、`target`プロパティが登場しました。これは、デプロイ先が静的ホスティングなのか、サーバーホスティングなのかを書くことでビルド時に適切な書き出しをしてくれます。値は、`server`と`static`から選べます。

ここで以下のようにしたらどうなるの？と思うかと思いますが

```js:nuxt.config.js
export default {
    ssr: true,
    target: 'static',
    ...
}
```
、その前にもう一つ、SSG (`nuxt generate`)について説明します。

## SSG(Static Site Generation)について
今まで、SPAとSSRを見てきました。それぞれ、クライアント側とサーバー側でリクエストが来てからレンダリングをしていましたが、SSGは事前に静的ファイルを生成します。Nuxtでは`nuxt generate`というコマンドを走らせることで、静的ファイルが初期設定では/distフォルダに出力されます。動的ルーティングに関しては、書き出し時に全てのルートを書き出します。

事前にビルド/レンダリングしている分、ユーザーのアクセスから最も早くコンテンツを送ることができ、さらにCDN(Content Delivery Network)を使うことで適切なキャッシュを用いてさらなるパフォーマンスアップもできます。

ただし、レンダリング（静的ファイル生成）自体は、頻繁に行うものではないので、動的ルーティングを伴う頻繁に更新するブログページや管理者が更新を管理できないWebアプリ（ユーザー投稿型）には適していないでしょう。


ここまでのまとめをするとこんな感じです。

| 名前 | いつレンダリングするか | どこでレンダリングするか |
|:-:|:-:|:-:|
| Static Site Generation | 事前（1度のみ） | - |
| Server Side Rendering | アクセス時(都度) | サーバー |
| Single Page Application(CSR) | アクセス時(都度) |  クライアント |

## ssrとtargetとnuxt generate
さて、ここからややこしくなります。上のテーブルでは3パターンしか結局ないように見えますが、組み合わせを見てみると

**ssr**プロパティがtrue/falseで2通り、**target**プロパティが`"static"`/`"server"`の2通り。この時点で組み合わせは4通り存在しています。
ここから、先は実際にビルドログを見ながらどういう挙動をするか見ましょう。

プロジェクトツリーはこんな感じです。 (あまり必要ないところは省いています。)

```
├── README.md
├── assets
├── components
│   ├── Logo.vue
│   └── README.md
├── dist
├── layouts
│   ├── README.md
│   └── default.vue
├── middleware
├── node_modules
├── nuxt.config.js
├── package.json
├── pages
│   ├── README.md
│   ├── index.vue
│   └── user
│       └── _id.vue
├── plugins
├── static
├── store
└── yarn.lock
```

### `ssr:true` × `target: "server"`
```
❯ nuxt build
ℹ Production build 
ℹ Bundling for server and client side
ℹ Target: server
✔ Builder initialized
✔ Nuxt files generated

✔ Client
  Compiled successfully in 5.53s

✔ Server
  Compiled successfully in 1.15s


Hash: 64fc26570db2fef26058
Version: webpack 4.44.2
Time: 5533ms
Built at: 12/08/2020 5:53:05 PM
                         Asset       Size  Chunks                         Chunk Names
../server/client.manifest.json   7.84 KiB          [emitted]              
                    45171b8.js   51.7 KiB       0  [emitted] [immutable]  app
                    7bd210d.js  418 bytes       3  [emitted] [immutable]  pages/user/_id
                    9f7d307.js    163 KiB       1  [emitted] [immutable]  commons/app
                      LICENSES  389 bytes          [emitted]              
                    b466dc3.js    2.3 KiB       4  [emitted] [immutable]  runtime
                    bcbe870.js   2.93 KiB       2  [emitted] [immutable]  pages/index
 + 2 hidden assets
Entrypoint app = b466dc3.js 9f7d307.js 45171b8.js

Hash: e739de48216ea4769b6b
Version: webpack 4.44.2
Time: 1181ms
Built at: 12/08/2020 5:53:06 PM
               Asset       Size  Chunks             Chunk Names
      pages/index.js   11.4 KiB       1  [emitted]  pages/index
   pages/user/_id.js    1.9 KiB       2  [emitted]  pages/user/_id
           server.js   81.1 KiB       0  [emitted]  app
server.manifest.json  303 bytes          [emitted]  
 + 3 hidden assets
Entrypoint app = server.js server.js.map
ℹ Ready to run nuxt start
✨  Done in 11.20s.
```

サーバーとクライアント両方のファイルがコンパイルされていますね。コンパイルされた`server.js`はサーバーサイドレンダリングに使用されるファイルかと思われます。
最後の`Ready to run nuxt start`の通り`nuxt start`行うと、サーバーサイドレンダリングの待ち受けが始まります。
これは通常のSSRで間違いありません

---

次に、
### `ssr:true` × `target: "static"` 
先ほどとは、targetを変えました。デプロイ先はstatic（静的ホスティング）といっておいて、SSRをすると伝えるとどうなるか。

```
❯ nuxt build
ℹ Production build
ℹ Bundling for server and client side
ℹ Target: static
✔ Builder initialized
✔ Nuxt files generated

✔ Client
  Compiled successfully in 5.48s

✔ Server
  Compiled successfully in 576.81ms


Hash: a07299f5e74a91e41ae3
Version: webpack 4.44.2
Time: 5485ms
Built at: 12/08/2020 6:00:44 PM
                         Asset       Size  Chunks                         Chunk Names
../server/client.manifest.json   7.87 KiB          [emitted]              
                    0ef3050.js    163 KiB       1  [emitted] [immutable]  commons/app
                    150697f.js   55.9 KiB       0  [emitted] [immutable]  app
                    429343b.js  418 bytes       3  [emitted] [immutable]  pages/user/_id
                      LICENSES  389 bytes          [emitted]              
                    d4471ab.js   2.93 KiB       2  [emitted] [immutable]  pages/index
                    f7a4269.js    2.3 KiB       4  [emitted] [immutable]  runtime
 + 2 hidden assets
Entrypoint app = f7a4269.js 0ef3050.js 150697f.js

Hash: e0a3e4234ff4af37a94b
Version: webpack 4.44.2
Time: 579ms
Built at: 12/08/2020 6:00:45 PM
               Asset       Size  Chunks             Chunk Names
      pages/index.js   11.4 KiB       1  [emitted]  pages/index
   pages/user/_id.js    1.9 KiB       2  [emitted]  pages/user/_id
           server.js   82.8 KiB       0  [emitted]  app
server.manifest.json  303 bytes          [emitted]  
 + 3 hidden assets
Entrypoint app = server.js server.js.map
ℹ Ready to run nuxt generate 
```
上部にあるメッセージを見ると、サーバーとクライアントのコンパイルはされたようです。
しかし、この状態で`nuxt start`をすると、設定が`static`の場合は`nuxt generate`をしないと`/dist`がないから、出来ないと怒られます。

``` 
FATAL  Output directory dist/ does not exists, please use nuxt generate before nuxt start for static target. 
```

ここから察するに、`ssr`プロパティはコンパイル用の設定、`target`はデプロイ時に`nuxt start`をした際、server設定ならSSRを行う用の設定かと思われます。
ちなみに、ビルドログの最後に`Ready to run nuxt generate`とあるように静的ファイルを書き出すのが正しい流れのようです。(`nuxt generate`の結果はのちほど...)

---
### `ssr: false` ×　`target: "server"`
Nodeサーバーにデプロイするものの、サーバーレンダリングは行わずSPAですという設定にすると...

```
❯ nuxt build
ℹ Production build 
ℹ Bundling only for client side 
ℹ Target: static
✔ Builder initialized  
✔ Nuxt files generated 

✔ Client
  Compiled successfully in 5.17s


Hash: 0276d42045ba56c3f857
Version: webpack 4.44.2
Time: 5168ms
Built at: 12/08/2020 6:13:49 PM
                         Asset       Size  Chunks                         Chunk Names
../server/client.manifest.json   7.81 KiB          [emitted]              
                    1744167.js   51.9 KiB       0  [emitted] [immutable]  app
                    7bd210d.js  418 bytes       3  [emitted] [immutable]  pages/user/_id
                    9f7d307.js    163 KiB       1  [emitted] [immutable]  commons/app
                      LICENSES  389 bytes          [emitted]              
                    b466dc3.js    2.3 KiB       4  [emitted] [immutable]  runtime
                    bcbe870.js   2.93 KiB       2  [emitted] [immutable]  pages/index
 + 1 hidden asset
Entrypoint app = b466dc3.js 9f7d307.js 1744167.js
ℹ Generating output directory: dist/ 
ℹ Generating pages 
✔ Generated route "/"
✔ Client-side fallback created: 200.html   
```
コンパイルはクライアントのみされました。
targetは`server`にしたのですが、勝手に`ｓtatic`にされました。
`ssr:false`ならサーバーが必要ないと判断して、staticで書き出すようですね。静的ファイルが/distフォルダにも書き出されていました。

最後に

---
### `ssr: false` ×　`target: "static"`
```
❯ nuxt build
ℹ Production build
ℹ Bundling only for client side 
ℹ Target: static 
✔ Builder initialized  
✔ Nuxt files generated  

✔ Client
  Compiled successfully in 4.94s


Hash: 0276d42045ba56c3f857
Version: webpack 4.44.2
Time: 4941ms
Built at: 12/08/2020 6:19:07 PM
                         Asset       Size  Chunks                         Chunk Names
../server/client.manifest.json   7.81 KiB          [emitted]              
                    1744167.js   51.9 KiB       0  [emitted] [immutable]  app
                    7bd210d.js  418 bytes       3  [emitted] [immutable]  pages/user/_id
                    9f7d307.js    163 KiB       1  [emitted] [immutable]  commons/app
                      LICENSES  389 bytes          [emitted]              
                    b466dc3.js    2.3 KiB       4  [emitted] [immutable]  runtime
                    bcbe870.js   2.93 KiB       2  [emitted] [immutable]  pages/index
 + 1 hidden asset
Entrypoint app = b466dc3.js 9f7d307.js 1744167.js
ℹ Ready to run nuxt generate  
```
変わらないかと思ったら**微妙に違います。**
最後に、/distフォルダへの書き出しをしなくなりました。代わりに、最後に`Ready to run nuxt generate`の一言が入りました。


### ここまでのまとめ
`ssr`プロパティは、サーバーサイドレンダリングの有無。
`target`プロパティは、デプロイ先を考えて走らせるコマンドの用意または実行と言ったところでしょうか。
まだ挙動が完全にはわからないです。


## `ssr: true` × `nuxt generate` ??
`ssr: false`の場合は、どちらも最後に`nuxt generate`を促す、ないしは実行するようなものになっていました。これは、考えてみれば当たり前で、SPAの場合はクライアント側でレンダリングするためNuxtにおけるサーバーの意味合いがなくなり静的ファイルとしておけばいいだけです。
では、`ssr: true`の時の`nuxt generate`と`ssr: false`の時の`nuxt generate`はどう違うのでしょう？

実行してみます。
まず、
#### `nuxt generate` with `ssr:true`

```html:index.html
<!doctype html>
<html data-n-head-ssr>
  <head>
    <title>spa ssr test</title><meta data-n-head="ssr" charset="utf-8"><meta data-n-head="ssr" name="viewport" content="width=device-width,initial-scale=1"><meta data-n-head="ssr" data-hid="description" name="description" content=""><link data-n-head="ssr" rel="icon" type="image/x-icon" href="/favicon.ico"><link rel="preload" href="/_nuxt/e551a4f.js" as="script"><link rel="preload" href="/_nuxt/70c4cdc.js" as="script"><link rel="preload" href="/_nuxt/d62cb5f.js" as="script"><link rel="preload" href="/_nuxt/d6be693.js" as="script"><link rel="preload" href="/_nuxt/f264199.js" as="script"><style data-vue-ssr-id="fa7ff0ca:0 56b15182:0 1b7833da:0 1930a9a0:0">.nuxt-progress{position:fixed;top:0;left:0;right:0;height:2px;width:0;opacity:1;transition:width .1s,opacity .4s;background-color:#000;z-index:999999}.nuxt-progress.nuxt-progress-notransition{transition:none}.nuxt-progress-failed{background-color:red}html{font-family:"Source Sans Pro",-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;font-size:16px;word-spacing:1px;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%;-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;box-sizing:border-box}*,:after,:before{box-sizing:border-box;margin:0}.button--green{display:inline-block;border-radius:4px;border:1px solid #3b8070;color:#3b8070;text-decoration:none;padding:10px 30px}.button--green:hover{color:#fff;background-color:#3b8070}.button--grey{display:inline-block;border-radius:4px;border:1px solid #35495e;color:#35495e;text-decoration:none;padding:10px 30px;margin-left:15px}.button--grey:hover{color:#fff;background-color:#35495e}.container{margin:0 auto;min-height:100vh;display:flex;justify-content:center;align-items:center;text-align:center}.title{font-family:Quicksand,"Source Sans Pro",-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;display:block;font-weight:300;font-size:100px;color:#35495e;letter-spacing:1px}.subtitle{font-weight:300;font-size:42px;color:#526488;word-spacing:5px;padding-bottom:15px}.links{padding-top:15px}.NuxtLogo{-webkit-animation:appear 1s;animation:appear 1s;margin:auto}@-webkit-keyframes appear{0%{opacity:0}}@keyframes appear{0%{opacity:0}}</style><link rel="preload" href="/_nuxt/static/1607419986/payload.js" as="script"><link rel="preload" href="/_nuxt/static/1607419986/manifest.js" as="script">
  </head>
  <body>
    <div data-server-rendered="true" id="__nuxt"><!----><div id="__layout"><div><div class="container"><div><svg width="245" height="180" viewBox="0 0 452 342" xmlns="http://www.w3.org/2000/svg" class="NuxtLogo"><path d="M139 330l-1-2c-2-4-2-8-1-13H29L189 31l67 121 22-16-67-121c-1-2-9-14-22-14-6 0-15 2-22 15L5 303c-1 3-8 16-2 27 4 6 10 12 24 12h136c-14 0-21-6-24-12z" fill="#00C58E"></path> <path d="M447 304L317 70c-2-2-9-15-22-15-6 0-15 3-22 15l-17 28v54l39-67 129 230h-49a23 23 0 0 1-2 14l-1 1c-6 11-21 12-23 12h76c3 0 17-1 24-12 3-5 5-14-2-26z" fill="#108775"></path> <path d="M376 330v-1l1-2c1-4 2-8 1-12l-4-12-102-178-15-27h-1l-15 27-102 178-4 12a24 24 0 0 0 2 15c4 6 10 12 24 12h190c3 0 18-1 25-12zM256 152l93 163H163l93-163z" fill="#2F495E"></path></svg> <h1 class="title">
      SPA SSR TEST
    </h1> <div class="links"><a href="https://nuxtjs.org/" target="_blank" rel="noopener noreferrer" class="button--green">
        Documentation
      </a> <a href="https://github.com/nuxt/nuxt.js" target="_blank" rel="noopener noreferrer" class="button--grey">
        GitHub
      </a></div></div></div></div></div></div><script>window.__NUXT__={staticAssetsBase:"/_nuxt/static/1607419986",layout:"default",error:null,serverRendered:!0,routePath:"/",config:{}}</script><script src="/_nuxt/e551a4f.js" defer></script><script src="/_nuxt/f264199.js" defer></script><script src="/_nuxt/70c4cdc.js" defer></script><script src="/_nuxt/d62cb5f.js" defer></script><script src="/_nuxt/d6be693.js" defer></script>
  </body>
</html>
```

めちゃくちゃ見づらいですが、`<body>`タグ内にNuxtのデフォルトのDocumentationやGithubのリンクが見えますね。
思った通りの静的ファイルが出ています。



次に
#### `nuxt generate` with `ssr: false`

```html:index.html
<!doctype html>
<html>
  <head>
    <title>spa ssr test</title><meta data-n-head="1" charset="utf-8"><meta data-n-head="1" name="viewport" content="width=device-width,initial-scale=1"><meta data-n-head="1" data-hid="description" name="description" content=""><link data-n-head="1" rel="icon" type="image/x-icon" href="/favicon.ico"><link rel="preload" href="/_nuxt/94ddf8e.js" as="script"><link rel="preload" href="/_nuxt/6686901.js" as="script"><link rel="preload" href="/_nuxt/fe66e19.js" as="script"><link rel="preload" href="/_nuxt/19e90ea.js" as="script">
  </head>
  <body>
    <div id="__nuxt"><style>#nuxt-loading{background:#fff;visibility:hidden;opacity:0;position:absolute;left:0;right:0;top:0;bottom:0;display:flex;justify-content:center;align-items:center;flex-direction:column;animation:nuxtLoadingIn 10s ease;-webkit-animation:nuxtLoadingIn 10s ease;animation-fill-mode:forwards;overflow:hidden}@keyframes nuxtLoadingIn{0%{visibility:hidden;opacity:0}20%{visibility:visible;opacity:0}100%{visibility:visible;opacity:1}}@-webkit-keyframes nuxtLoadingIn{0%{visibility:hidden;opacity:0}20%{visibility:visible;opacity:0}100%{visibility:visible;opacity:1}}#nuxt-loading>div,#nuxt-loading>div:after{border-radius:50%;width:5rem;height:5rem}#nuxt-loading>div{font-size:10px;position:relative;text-indent:-9999em;border:.5rem solid #f5f5f5;border-left:.5rem solid #000;-webkit-transform:translateZ(0);-ms-transform:translateZ(0);transform:translateZ(0);-webkit-animation:nuxtLoading 1.1s infinite linear;animation:nuxtLoading 1.1s infinite linear}#nuxt-loading.error>div{border-left:.5rem solid #ff4500;animation-duration:5s}@-webkit-keyframes nuxtLoading{0%{-webkit-transform:rotate(0);transform:rotate(0)}100%{-webkit-transform:rotate(360deg);transform:rotate(360deg)}}@keyframes nuxtLoading{0%{-webkit-transform:rotate(0);transform:rotate(0)}100%{-webkit-transform:rotate(360deg);transform:rotate(360deg)}}</style><script>window.addEventListener("error",function(){var e=document.getElementById("nuxt-loading");e&&(e.className+=" error")})</script><div id="nuxt-loading" aria-live="polite" role="status"><div>Loading...</div></div></div><script>window.__NUXT__={config:{}}</script>
  <script src="/_nuxt/94ddf8e.js"></script><script src="/_nuxt/6686901.js"></script><script src="/_nuxt/fe66e19.js"></script><script src="/_nuxt/19e90ea.js"></script></body>
</html>
```

先ほどとは、違ったファイルが出来上がりました。読み解くと、ローディングが表示されそうなことがわかります。
そうです。これはSPA用のクライアントでレンダリングをする静的ファイルを書き出しています。というか先ほどの
`ssr: false` × `target: "server"`の時に生成されたファイルと全く同じです。

つまり、`nuxt generate`は実行時の`nuxt.config.js`の`ssr`プロパティを見て、それに準じた静的ファイルを生成します。
`ssr: true`の場合は内容まで完成されたHTML、`ssr: false`の場合はSPA用のHTMLが出力されます。
（Nuxt始めたての僕はSPAのファイルは静的ファイルとは別だと考えていたのでここでつまずきました。）

## まとめ
上記を踏まえて、テーブルにすると

| 名前 | いつレンダリングするか | どこでレンダリングするか |
|:-:|:-:|:-:|
| Static Site Generation(ssr: true) | 事前（1度だけ） | - |
| Static Site Generation(ssr: false) | アクセス時(都度) | クライアント |
| Server Side Rendering | アクセス時(都度) | サーバー |
| Single Page Application(CSR) | アクセス時(都度) | クライアント |

というのが正しそうです。
SPA時には、サーバーでのレンダリングが不要なため、常に静的ファイルが出力されます。そのため、`ssr: false`のSSGと挙動的には変わらなくなります。

ですので、クライアントサイドでレンダリングをさせたくない静的ファイル生成の時は`ssr: true`に設定するのをお忘れなく。