◆ 全ページで開くときに API を呼び出したりログイン・権限関係があったりすると SPA の面倒さが目立つようになってくる
◆ そういう場合は 普通にページ遷移して ページごとにクライアントサイドレンダリングで良さそう
  ◆ キャッシュがあればリソースのダウンロード時間はほぼなし
  ◆ パース・実行が重いライブラリ使ってなければ あまり変わらないと思う

SPA がつらいときもある

最近は何を作るも SPA です
ですが 普通にページ遷移するほうが楽だなと思うケースもありました

これまでだと 基本一般公開で誰でも見れて誰でも同じ内容というものがメインでした
API 呼び出しすらなく ページは切り替わるものの サーバは静的ファイルを返すだけというものも多かったです

こういうのでは SPA がすごく向いてると思います

ただ ログインしたり そのユーザの権限に応じて画面内容や開けるページが違うというのを作ったときは すごく面倒でした
通常のページ遷移だと考えなくてよかった部分で考えることが増えます

例えばログアウトして ログイン画面に移動したあとに戻るとどうなるかとかです
ログアウト時に History API や LocalStorage の state は破棄しないといけないです
SPA じゃないならこれらに状態を保存してないので気にする必要がないです

戻ったときには アプリケーション内のロード中画面は出そうです
API のレスポンスを受け取ってから 権限がないとわかりエラー画面を出したり どこかにリダイレクトしたりでしょう
内部データは見えなくてもロード画面すら見せたくないこともあります

未ログインで直接ログイン済みページの URL にアクセスした場合でも サーバが静的ファイルを返すだけならロード画面は出そうです
SPA だからというわけではないですが 静的ファイル + API の作りだと HTML を受け取る時点でリダイレクトや 400 系のレスポンスになったりしません
最初の API 呼び出しの結果によって クライアントサイドでページ移動やエラー画面を作るわけです

また すべてのページで API を呼び出して結果を表示するという場合も SPA のメリットはそこまで大きくないです
サーバとの通信が発生する以上 待たされることはあります

なにがやりたいんだっけ

そもそも自分がやりたいのは何だったっけと思って考えてみると SPA にしたいのではなくクライアントサイドレンダリングがしたいのでした

サーバ側では HTML を扱いたくないということです
Node.js は標準機能では HTML エスケープの関数すらないですしね
テンプレートエンジンを使って 分岐したり繰り返したりとかで HTML を作るという処理をサーバでしたくないです
どうせその HTML をブラウザ内で書き換えたりリスナつけたりするんですから最初からブラウザ内だけに収めたいです
サーバがすることは 静的ファイルのサーブや API の結果を返すだけです

もし ボタンを押したら画面が変化するとかがほぼ無いページなら サーバでやっても良いと思います
ちょっと DOM を直接いじれば良い程度ならともかく ある程度複雑化してくると DOM 管理ライブラリに任せたくなってきます
そうなるとサーバサイドのテンプレートエンジンみたいなことをクライアントサイドでするわけです
二度手間です

自然と SPA に

クライアントサイドレンダリングだと 移動先のページを構築するスクリプトがすでに手元にあれば わざわざサーバにアクセスする必要はありません
ページ固有のデータが必要でも API からデータを受け取れば それだけで別の画面に書き換えられます

通常のページ遷移をすると HTML をロードしなおして ライブラリやページを構築するスクリプトもロードすることになって無駄が多いです
通信部分はキャッシュされるとしても HTML/CSS/JavaScript のパースや画面反映や実行などは避けれません
なので自然と SPA にもしていました

SPA にしても API レスポンスが遅いと

SPA でも全部のページで API から情報取得が必要なら 待機時間が発生し そのロード中は何もできません
最近は SPA ページを良く見ますが 重いサイトだと結局ぐるぐる回ってるロード画面を見続けることになります
API 呼び出しで数秒かかるような場合だと ライブラリのロードが入っても誤差レベルです

地図などのロードも実行も遅くなる重いライブラリを使ってるならともかく クライアントサイドレンダリングをサポートしてくれる軽量ライブラリくらいだと気にする必要あるのかなという気はしています

開発中に a タグのクリック時の preventDefault 処理を忘れていて SPA にならず 普通のページ遷移になってることがときどきあります
ですが 見た目ではそれに気づけないことがほとんどです
どっちも一瞬なので devtools の Network タブで通信が発生してるかを見て判断してるほどです

そういうこともあって SPA にせず 通常のページ遷移付きでクライアントサイドレンダリングでもいいかなと思ってます

SPA にしない場合

SPA にしない場合ですが ひとつは単純に SPA と同じままで ページ移動を通常のページ遷移にするものです
History API を使わないだけです

これでも動きますが 毎回サーバにリクエストを送るのなら API を別々に呼び出す必要はない気がします
HTML に JSON を埋め込んでおいて API から取得する代わりに それを使うようにすれば API リクエストを省略できます

こういう感じです

<!DOCTYPE html>
<meta charset="utf-8"/>

<script type="application/json" id="data">
{{ここにページごとのデータを JSON で埋め込む}}
</script>

<script type="module" src="index.js"></script>

HTML をサーバで操作することになるといえばそうですが 画面に影響する body 部分には一切触れませんし ただの文字列結合や文字列置換で済むものです
やりたくないのは クライアントサイドでもやるような HTML ならではの画面を作る部分なので データを埋め込むくらいなら気になりません

HTML ファイルが静的ファイルではなくなるけど

HTML ファイルが静的ファイルではなくなるデメリットはあるのですが HTML ファイルなので別に構わないかなと思います
静的ファイルだと 変更がない限りずっと同じ中身なので キャッシュを使えます
しかし HTML ファイルがキャッシュされたままだと更新時に反映されず困ります
なので HTML ファイルは短期キャッシュやキャッシュ無しにしておき JavaScript 等のリソースファイルをキャッシュ対象にします
それらのファイルはファイル名にハッシュ値をつけたりして一意な URL にしてから サーバへのリクエスト不要の長期キャッシュを有効にします
更新時には HTML ファイル中の URL を変更することで 新しいバージョンに切り替えられます
そういうわけで HTML ファイルはほぼキャッシュしないので 静的ファイルじゃなくなっても特に困りません

クライアントサイドレンダリングでは HTML はとても軽量なので 毎回 HTML を受信しても負担は少ないです
HTML にデータを埋め込むことで軽量ではなくなりますが HTML が軽量であっても キャッシュしない API 呼び出しを別に行うなら同じことです
それどころか HTML と JavaScript のパースが終わってから 別リクエストで fetch する API 呼び出しのほうが画面が見れるようになるまでにかかる時間は長くなります

それに SPA で静的ファイルと API という作りでも HTML は動的というケースも十分見かけます
例えば アプリケーションを配置するルートになるパスを base タグに埋め込んだりです
base タグだと 最初から埋め込まれてないと アクセスする API の URL すらわからなくなりますからね
もちろん クライアントサイドのアプリケーションに固定で埋め込んだり アプリケーションをサーバのルートに配置するなどで base タグを使わないケースもありえます

ルーティングも不要

上の例だと JavaScript は常に index.js をロードしています
index.js では title を設定したり URL をもとにルーティングして ページ用のモジュールをロードすることを想定しています
ただ サーバサイドでページごとのデータがわかる以上 そのページ用の JavaScript モジュールもわかるはずです
なので src のファイル名もサーバサイドで埋め込んでしまっても良いかもしれません
ルーティング処理と 1 ファイルのロードを減らせます

埋め込み箇所を 1 つの JSON だけにしたいなら JSON の中にエントリポイントになるモジュールのパスを追加しておいて

<script type="module">
const data = JSON.parse(document.getElementById("data").innerHTML)
import(data.module_path).then(mod => mod.main(data))
</script>

のようにロードさせることもできます

ページごとに毎回 JavaScript をロードすることになるので Webpack で全部入りで作ると無駄が多いです
ES Modules でロードすると ページで必要なモジュールだけで済むので ページ遷移が発生してもロードし直すのは最小限で済ませられます
リクエスト数はかなり増えますけど……

SPA で埋め込みもできるけど

初回の API 呼び出しを減らすだけなら SPA にした上で 最初のアクセスだけ HTML に API レスポンスを埋め込んで置くこともできます
ただ そうなると最初に開いたときとページ遷移時で動きが変わります
ページを開くパフォーマンス重視ならそれがベストになるのかもですが 作るのが面倒な上 複雑化するのでどちらかに揃えたいところです
なので SPA なら初回アクセスでも API を使って SPA にしないなら API は使わず そもそも API という形で用意すらしないことにします

それに その方法じゃログイン関係での面倒さが解決できませんからね
開く権限の無いページを開こうとしたら サーバからエラーのレスポンスが返されて 本来のそのページの JavaScript を実行できないほうが好ましいです

比較

SPA (静的ファイル + API) とクライアントサイドレンダリングだけで ページを開くまでの処理内容を比較してみます
ブログ管理画面の記事一覧のページを開いたときを想定します

SPA (静的ファイル + API): 初回アクセス

1. HTML ファイルを受信
2. アカウント情報を取得する API にリクエストして情報を取得
3.
a. 未ログインや権限不足 → エラー表示やリダイレクト
b. ページを表示可能 → アカウント情報を保持して 4 へ
4. 記事一覧を取得する API にリクエストして情報を取得
5. 受け取ったデータを使って画面を構築

SPA (静的ファイル + API): JavaScript で遷移してきたとき

上の 2 までは終わっているので 3 から行います

1.
a. 未ログインや権限不足 → エラー表示やリダイレクト
b. ページを表示可能 → アカウント情報を保持して 2 へ
2. 記事一覧を取得する API にリクエストして情報を取得
3. 受け取ったデータを使って画面を構築

HTML に JSON を埋め込んで クライアントサイドレンダリング

必要なデータは HTML に含まれていて 権限がないなら HTML の中身がそうなってるかリダイレクトのレスポンスになってます
アプリケーション用の HTML を受信した時点で クライアントサイドのチェックは不要です

1. HTML ファイルを受信
2. HTML 中に埋め込まれてるデータを使って画面を構築

結果

SPA (静的ファイル + API) にしないほうがシンプルですね
結局サーバ側でやってるわけなのですが サーバ側が API だけでも認証・権限確認やルーティング処理などはサーバサイドにもあります
なのでここまでサーバ側でやってもそれほど負荷は大きくないです
API で JSON を返す代わりに HTML ファイルに JSON を埋め込んで返す程度のものです

また 完全に「静的ファイル + API」ではなく リダイレクトなどは サーバからのレスポンスで直接行いたい場合も出てきます
そうなると API 以外の ユーザ向けのページの URL をサーバ側でも知る必要が出てきます
サーバとクライアントで二重管理になります
それならサーバ側だけでやって クライアント側でまで管理しないほうが簡単です

SPA とではなく 昔ながらの サーバサイドレンダリングと比べたほうがわかりやすいかもです
テンプレートエンジンで HTML をサーバサイドで作るのが面倒なのと 作っても結局クライアントサイドで変更したり リスナつけたりするなら最初からクライアントサイドでやれば良い それだけなんです

フォームは使わない

API は使わないと書きましたが POST だけは例外です
フォームの submit を許すと 不正データの場合にフォームの中身を復元して エラーメッセージをつけてということをしないといけなくて面倒です
あと POST だと ページリロードで再度 POST することになるので 対処が必要です

それらの面倒をなくすために 通常のページ遷移をすると言っても フォームの submit は使いません
JavaScript で API に POST して レスポンスに応じて

const res = await fetch(api_endpoint, options)
const data = await res.json()
if (data.ok) location = data.url

という感じでページ遷移します

エラーだったら同じ画面のまま エラーメッセージを表示します

記事の中身を取得みたいな GET の API は HTML に埋め込まれてなくなりますが POST の API は残します

まとめ

HTML ごとロードし直しでも 2 回目以降はディスクやメモリキャッシュが使われるので 時間がかかるのは ダウンロード以外の HTML や JavaScript のロード部分です
クライアントサイドレンダリングなら HTML はほぼ空ですし JSON を埋め込んでも それは API で取得するデータと同じ量なので SPA と変わりません
ほとんど JavaScript 次第です
重たいライブラリとかがなければ そこまで変わらないと思います
それに SPA でも API のレスポンスが遅ければずっとロード画面で待たされて結局何もできませんし SPA のメリットになりません

そういう感じで 誰が開いても同じ画面で API 呼び出しがあまりないなら SPA でいいのですが 毎ページで API 呼び出しや 認証が必要など SPA のメリットが減ってくると 通常のページ遷移をするけど HTML はブラウザで作るというのでいいかなと思ってます

追記:その他のメリット・デメリット

少し試したり SPA のサイトを見てて感じたメリットやデメリットについてです
しばらくの間はちょっとした内容ならここへ追記予定です

スクロールバー

ページバック時のスクロールバーの位置の復元については SPA の History API を使う方法でも自動で復元されました

復元できないケースもありましたが 現状では通常のページ遷移でも復元できないものなので スクロールバーの復元については SPA にするしないを気にしなくて良さそうです

復元ではなく保持については SPA にメリットがありました
サイドバーにページ一覧があり サイドバーの中にスクロールバーがあるようなページです
適当にスクロールしてからページを開いたときに SPA だとサイドバーの位置は保持されています
軽くページを見てから他のページも次々に見ていきたい場合 サイドバーのスクロール位置が保持されていると助かります
スクロール位置だけじゃなく折りたたみ機能があるならその状態もです
ページ遷移ごとに毎回一番上に戻って折りたたまれてると不便なんですよね

ページの書き換えが部分書き換えで済んで それ以外の部分を保持したほうが良いときは SPA のほうが優れてます
通常のページ遷移でやるなら SessionStorage にそれらの状態を入れて 次のページがロードされてから復元なので実装が手間になります

beforeunload の確認

フォームの入力途中でページを切り替えようとしたときに 移動してよいかの確認ダイアログを出す機能があります
ページ内で管理してるボタンやリンクのクリックなら イベントを受け取って専用の画面を作ったりもできますが ブラウザの戻るボタンで外部のサイトに戻るときやリロードボタンなど beforeunload に頼らざるを得ないときがあります
独自に確認画面を作っても一貫性がなくなるのですべて beforeunload に任せてしまいたいです
しかし SPA だとページ内で DOM を書き換えるだけなのでこのイベントは起きず 確認なしでフォームを消して別の画面に書き換えてしまいます
beforeunload に加えて JavaScript でのページ移動時のチェック処理まで必要になります
さらに beforeunload のダイアログと完全に同じダイアログを出すことはできず一貫性に欠けます

この点では SPA にしないほうがメリットがあります

新しいタブで開く

ブログみたいなサイトだと 一覧画面で各記事のページをタブで開くことは多いと思います
通常のナビゲーションはページャの切り替えとか一部だけです

こういうサイトだと SPA のメリットはあまりないです
ただデメリットもそこまでありません
リソースファイルはキャッシュされていてリクエストは発生しないです
ES Modules で全部ロードしたり ページごとにファイル分割してバンドルしていると SPA にしない場合と同じようなものです
webpack で全部まとめてしまっていると ダウンロードが不要でも大きめの JavaScript のパースなどが必要なので少し表示が遅いかもです

ページ共通情報

ログインするサイトだとログイン中ユーザ情報など ページ間で引き継げば良いデータがあります
SPA だと 最初に開いたときに取得すれば都度取得しなくても良いです
しかしそれだとユーザ情報が変わったとしてもリロードするまで古い情報です

数分ごとなど定期的に確認して更新したり リクエストのたびに毎回合わせて取得が必要になると結構面倒です
ページを表示するために必要なものを毎回全部取得するなら SPA のメリットが少なくなります
もう SPA にしなくても良くない?と思います

ページごとの API

API の作りに関してです

サーバのデータを API で取得となると サーバ側はリソースをベースに API を作れます
クライアントは表示したい内容に必要な API を必要なだけ呼びだして結果を結合します
外部のサービスを使うときに近いですし そのサーバを使うアプリケーションが増える可能性があるなら有効です

ただ 実際にはそういうことはあまりなくて そのクライアント専用にサーバも一緒に作る感じです
サーバが先にあってそのデータを使うクライアントを作るのではなく こういう画面でこういう操作ができるクライアントが欲しいとなってそのためにサーバも作ります
そういう場合だとリソースベースな API にする必要があまりなく 逆に複雑になります

まず 1 つのページを開くのにいくつかのリクエストが必要です
また権限関係はリソースベースではなく 操作画面であるページベースで何ができると決めることが多いです
リソースベースで作るとその権限をリソースに当てはめる必要があって面倒が増えます
1 つのリソースの中でもデータでもこれは誰でも書き換えられて これは権限持ってる人だけみたいなのが多いと そのリソースをまとめて更新できる API でやろうとしたら内部処理が複雑です
ページのボタン操作ごとに API を分けていたらリクエスト単位で OK/NG を決めてしまえます

結果 API はページごとになることが多いです
サーバ側でもクライアントのルーティングと そのページ用のデータを準備する API を作ります
そうなるとサーバ側は SPA にしない場合とあまり違いがないです
JSON を返す代わりに JSON を HTML に埋め込んだレスポンスを返すくらいです
クライアント側で通常のナビゲーションにすれば切り替えれば完了です

これももう SPA じゃなくて良くない?と思います