141
100

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

Nuxt.jsAdvent Calendar 2020

Day 9

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

Last updated at Posted at 2020-12-08

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に以下のように記述します。

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

modeプロパティはv2.14.5でdeprecatedになりました。代わりにこのssrプロパティを使いましょう。
詳しい説明はこちらを。The ssr Property

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

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

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にしたのですが、勝手に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 generatessr: falseの時のnuxt generateはどう違うのでしょう?

実行してみます。
まず、

nuxt generate with ssr:true

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

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.jsssrプロパティを見て、それに準じた静的ファイルを生成します。
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に設定するのをお忘れなく。

141
100
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
141
100

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?