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
に以下のように記述します。
export default {
ssr: true, // SSRする場合
...
}
export default {
ssr: false, // SPAの場合
...
}
mode
プロパティはv2.14.5でdeprecatedになりました。代わりにこのssr
プロパティを使いましょう。
詳しい説明はこちらを。The ssr Property
またv2.13.0から、target
プロパティが登場しました。これは、デプロイ先が静的ホスティングなのか、サーバーホスティングなのかを書くことでビルド時に適切な書き出しをしてくれます。値は、server
とstatic
から選べます。
ここで以下のようにしたらどうなるの?と思うかと思いますが
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
にしたのですが、勝手にstatic
にされました。
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
<!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
<!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
に設定するのをお忘れなく。