Web●●●○○

SPA

Single Page Application ・ えすぴーえー

最初にアプリ一式を読み込み、以降の画面遷移をブラウザ内のJavaScriptで行うWebの構成。

✦ 30秒で試す — 航海画面を切り替えても、ページは1枚も読み直さない足し上げた量で、SPA の方が少なくなるのは何回目から?予想してみる

概要

SPA(Single Page Application)は、最初のアクセスでHTMLとJavaScriptの塊を読み込み、以降の画面切り替えをすべてブラウザ内のJavaScriptで行うWebアプリの構成です。「シングルページ」という名前のとおり、サーバから受け取るHTMLの器は実質1枚だけで、リンクをクリックしてもページ全体の再読み込みは発生しません。JavaScriptが画面を描き換え、必要なデータだけをAPI経由で取り寄せます。

GmailやGoogleマップ、各種の管理画面など、「Webサイト」というより「アプリケーション」と呼びたくなるものの多くはSPAです。画面遷移のたびに白い画面を挟まない滑らかな操作感が持ち味で、React・Vue・SvelteといったフレームワークはこのSPAという文脈で発展してきました。これに対して、画面ごとにサーバがHTMLを返す従来の構成を MPA(Multi Page Application)と呼び分けます。

なぜ生まれたか

伝統的なWebでは、リンクをクリックするたびにブラウザがサーバへリクエストを送り、サーバがHTML全体を組み立てて返し、ブラウザが全部を描画し直していました。文書を読むには十分でも、メールクライアントのような操作の多いアプリでは、1クリックごとの全画面リロードは致命的にもたつきます。転機は2004〜2005年頃、GmailとGoogleマップが「ページを再読み込みせずにサーバと通信する」技術を駆使して、デスクトップアプリ並みの体験をブラウザ上で実現してみせたことでした。この手法は2005年に Jesse James Garrett によって Ajax と名付けられます。

ただし初期の Ajax アプリには弱点がありました。画面が変わっても URL が変わらないので、「戻る」ボタンが効かず、見ている画面をブックマークも共有もできません。これを解いたのが HTML5 の History API(history.pushState。2010年前後に主要ブラウザが対応)で、JavaScript から再読み込みなしに URL と履歴を書き換えられるようになりました。さらに、複雑化するUIコードを管理するフレームワーク(Backbone、AngularJS、そしてReact)が登場し、「画面遷移も含めてすべてブラウザ側で完結させる」構成が現実的になります。フロントエンドとバックエンドをAPIで分離すれば分業もしやすい — こうしてSPAは2010年代のWeb開発の主流アーキテクチャになりました。

詳細

何が往復するか — MPA との違い

SPAの動きは「重い初回」と「軽い2回目以降」の対比で理解できます。MPA は画面を移動するたびに、ページ全体の HTML を受け取って画面を捨て、白紙から描き直します。SPA は初回に、ほぼ空の HTML と、アプリ全体のコードを固めた JavaScript のバンドル(バンドラが多数のソースファイルを配信用にまとめたもの)を受け取ります。以降の移動では、画面の骨組みもコードも手元にあるので、API から JSON のデータだけを取り寄せ、変わる部分だけを描き換えます。

従来型(MPA)移動のたびにページ全体を受け取るSPA初回にアプリ一式、以降はデータだけ1回目HTML・CSS・JS 150KB2回目HTML 60KB(画面ごと)3回目HTML 60KB(画面ごと)1回目JSバンドル 450KB+HTML・JSON2回目JSON 5KB(データだけ)3回目JSON 5KB(データだけ)10回の合計690KB10回の合計502KB毎回: 画面を捨てる → 白くなる → 全部描き直す以降: 前の画面のまま → 変わる部分だけ描き換え初回は軽いが、使うほど積み上がる初回は重いが、使うほど償却される
何が往復するか — MPA は毎回ページ全体、SPA は初回にアプリ一式を払い、以降はデータだけ(棒の長さは転送量)

時間の流れで追うと、次のようになります。初回はブラウザがアプリを起動してからデータを取りに行き、以降の画面遷移では Web サーバには戻りません。

APIサーバWebサーバブラウザAPIサーバWebサーバブラウザ以降の画面遷移ではWebサーバに戻らない初回アクセスほぼ空のHTMLとJSバンドルJSを実行しアプリ起動データを要求JSONリンククリックでURLを書き換え必要なデータだけ要求JSON画面の差分だけ描き換え

初回の重さを、使う時間の長さで取り返す構造です。下の部品で確かめてください。タブを押すと2つのブラウザが同じ画面へ移動し、MPA は毎回白くなって HTML が積み上がり、SPA は初回に大きなバンドルを払ってから JSON だけで動きます。何度か行き来すると、累積のグラフで2本の線が交差します。

⚡ 体験: 同じ3画面を行き来して、初回コストが償却されるのを見る
サーバ従来型(MPA)about:blankまだ何も開いていないSPAabout:blankまだ何も開いていないタブはまだない。ここを押して開く直近の移動(押してから操作できるまで)MPA—SPA—
白い画面待ち(前の画面のまま)操作可能
累積
7 回目で逆転
従来型(MPA)SPA点線 = このまま続けた場合
SPA のバンドル
開いた回数0MPA 累積—SPA 累積—

どちらかのブラウザのタブ(受信箱・予定・設定)を押してください。2つのブラウザが同じ画面へ移動し、サーバから何が届くかが飛んできます。何度も行き来してみてください。

構成要素とエコシステム

SPAを支える柱は3つあります。第一にUIフレームワーク(React・Vue・Svelte)で、「データが変わったら画面がどう変わるか」を宣言的に書けるようにし、差分描画を引き受けます。その実現方法が、仮想DOM(木を作り直して比べる)やリアクティビティ(依存をたどって直接更新する)です。第二にクライアントサイドルーティングで、URL と画面の対応をブラウザ内で管理し、History API で「戻る」ボタンや直リンクを壊さないようにします。第三にバンドラ(Vite や webpack)で、多数のソースファイルを配信用のバンドルにまとめ、画面ごとに分割して必要なときに読み込めるようにします。バックエンドは画面のことを忘れ、REST APIやGraphQLでデータを供給することに専念できます。画面をまたいで共有するデータが増えると、その置き場所を決める状態管理も設計の中心になります。

弱点: 初期表示とSEO

SPAの代償は初回に集中します。JavaScriptをダウンロードし、実行し、さらにAPIの往復を待つまで、ユーザーには何も表示されません。バンドルが肥大化すると、この「白い画面の時間」は目に見えて伸びます。もうひとつの古典的な問題がSEO(検索エンジン最適化)で、初期HTMLがほぼ空であるため、JavaScriptを実行しないクローラーからは中身のないページに見えてしまいます。現在のGooglebotはJavaScriptを実行しますが、レンダリングの遅延や他の検索エンジン・SNSのプレビュー(OGP)を考えると、依然として不利は残ります。

この2つの弱点への回答が、初期表示分のHTMLをサーバ側で作ってしまうSSRや、ビルド時に作り置きするSSGです。Next.jsやNuxtといったフレームワークは「初回はサーバでHTMLを生成し、以降はSPAとして振る舞う」ハイブリッドを標準にしており、純粋なSPAとの境界は溶けつつあります。

実務の落とし穴

クライアントサイドルーティングには、MPA では起きなかった問題がいくつもあります。まず直リンクと再読み込みです。/settings のような URL はブラウザの中でだけ意味を持つので、その URL を直接開いたり再読み込みしたりすると、サーバには該当するファイルがなく 404 になります。どのパスにも同じ index.html を返す設定(フォールバック)が要ります。次に、ページが切り替わったことをブラウザが知らないので、スクロール位置の復元、フォーカスの移動、スクリーンリーダーへの「画面が変わった」という通知を、アプリ側で自分で面倒を見る必要があります。長時間開きっぱなしにされるので、後始末し忘れたイベントリスナーやタイマーによるメモリリークも表面化しやすくなります。

認証の扱いも変わります。API を呼ぶためのアクセストークンをブラウザの JavaScript が持つ構成では、localStorage に置いたトークンは XSS で盗まれます。HttpOnly の Cookie に置く、あるいはトークンをブラウザに渡さずサーバ側で持つ BFF(Backend for Frontend)構成にするなど、盗まれにくい置き場所を設計する必要があります。

使いどころの判断

SPAが輝くのは、ログイン後に長時間操作するアプリ — 管理画面、ダッシュボード、SaaSの業務ツール — です。SEOは不要で、初回ロードの重さは使用時間の長さで償却されます。逆に、ブログやECの商品ページのように「検索から来て1〜2ページ見て帰る」サイトでは、SPAの初期コストは払い損になりがちです。部品のグラフで、交差点より手前で帰ってしまう読者が多いサイトだと考えると分かりやすいでしょう。

試問

同じ3画面(受信箱・予定・設定)のアプリを、従来型(MPA: 画面ごとにサーバがページ全体を返す)と SPA(最初にアプリ一式の JavaScript を読み込み、以後はデータだけを取る)の2つのブラウザで同時に開きます。ブラウザの中のタブを押すと両方が同じ画面へ移動し、受け取った量の合計がグラフに積み上がります。SPA のバンドル(最初に読む JavaScript)の大きさは、開き始める前にスライダーで決められます。

✦ 命題挑戦中

利用者が画面をちょうど5回しか開かないとき、SPA の累積の転送量が MPA 以下になるようにせよ

  • 画面をちょうど5回開く(いま 0 回)
  • その時点で、SPA の累積転送量が MPA 以下
⚡ 体験: 同じ3画面を行き来して、初回コストが償却されるのを見る
サーバ従来型(MPA)about:blankまだ何も開いていないSPAabout:blankまだ何も開いていないタブはまだない。ここを押して開く直近の移動(押してから操作できるまで)MPA—SPA—
白い画面待ち(前の画面のまま)操作可能
累積
従来型(MPA)SPA
SPA のバンドル
開いた回数0MPA 累積—SPA 累積—

どちらかのブラウザのタブ(受信箱・予定・設定)を押してください。2つのブラウザが同じ画面へ移動し、サーバから何が届くかが飛んできます。何度も行き来してみてください。