TechFeed Experts Night Pick up

SPA化するMPAとMPA化するSPA ~TechFeed Experts Night#4 講演より

本記事は、2022年9月に開催された「TechFeed Experts Night#4 〜 フロントエンドアーキテクチャを語る」のセッション書き起こし記事「SPA化するMPAとMPA化するSPA(@yosuke_furukawa⁠⁠ ― TechFeed Experts Night#4 フロントエンドアーキテクチャを語る」を転載したものです。オリジナルはTechFeedをご覧ください。

古川と申します。@yosuke_furukawaでTwitterなどをやっております。

自己紹介

SPAは”見えるようになるまでが遅い”

私が今回お話するのは「MPA化するSPA」です。もともとSPAは、画面遷移(トランジション)をアプリケーションに合わせて最適化することを目的として発展した技術だと思っています。変更が発生したところだけレンダリングすることで高速化するテクニックだったのが、それをすべてのページで行うことで全体のUXを上げる - こういうふうに発展してきたのかなというところですね。

もともとSPAは

もうちょっとざっくり話をすると、ブラウザから何かしらのリクエストが発生してサーバからHTMLが返ってきます。そのときのHTMLにはまだ何も中身が入ってないので、中身のHTMLをもう1回構築するためにJSを取ってきて、JSが読み込まれて、最終的に表示がされて、そのときには操作もできるというのが、シングルページアプリケーションのクラシックな動きですね。

シングルページアプリケーションのクラシックな動き

シングルページアプリケーションの問題点としては、見えるようになるまでが若干重たく、遅いです。最初のタイミングでHTMLが返ってきているんだけど、コンテンツがあまり見えなくて、JSが読み込まれて初めて見えるという動きです。そのかわり、この後は速い。これがSPAの一番のポイントですね。つまり、最初は遅いんだけど、その後は速いのが特徴です。

シングルページアプリケーションの問題点

この問題(見えるようになるまでが遅い)を解決するためにNext.jsなどが取っているアプローチがSSR/SSG/ISRです。これらはすべて最初のHTMLを返す時点で内容も含めて返すというアプローチで、SSRはリクエストのときに作るし、SSGは事前に作っておくもの、ISRは事前に作っておいて後から更新するもの(ハイブリッドな動き)ですね。

要は、HTMLを何かしらのタイミングで作っておいて、それを返すということをやれば、どっちもいいとこ取りできるじゃん、という技術です。

SSR/SSG/ISRとは

MPA技術を使ってSPAを作る ―MPA化するSPA

SSRは普通にMPA(マルチページアプリケーション)でやっていたことなんで、MPAの技術を使ってSPAを作っている、これがMPA化するSPAのひとつの事例ですね。

どういうことになってるかというと、何かしらリクエストが走ります。クラシックなSSRを例に挙げると、SSRを実行して、実行が終わったらHTMLが返ります。その時点ではすでに描画されていて、その上でもう1回JSをロードするためにJSを取ってきてロードされます。こんな動き(下図)になります。

SSR

この時点でだいぶMPAとSPAのいいとこ取りをしていて、MPAの技術であるHTMLをサーバサイドでレンダリングするのと、ロードされた後はSPAの技術でハイブリッドな戦略を取る感じになっています。SPAだったけど、徐々にMPAに近寄っているなと…いう印象を持っていただければいいかなと思います。

MPAとSPAのいいとこ取りをしている

ただし、問題点が2つあるんですよ。ひとつは、SSRを実行したときの時間が伸びていること、もうひとつの問題がHTMLが描画されてからJSがロードされるまでの時間です。この2つに時間とコストがかかってしまいます。

問題1 ―SSR実行時の時間とコスト

SSRを実行しているときって、毎回リクエストしてる最中のオンザフライで作るというのはけっこう問題も大きくて、たとえばイベントループを止めちゃうとか、CPUコストが高いとか、結果、サーバからHTMLが返るまでのコストがかかってしまいます。

かといってSSGならどうかというと、SSGは事前に全部作らないといけないので、たとえば30万ページあれば、30万ページ分を一気に作らないといけないので、あまり現実的ではないです。

ではISRはどうかというと、結局これはキャッシュなので、最初にリクエストしたときに作り直す時間がかかってしまいますし、かつ、revalidateと呼ばれるキャッシュを再検証する期間をどうするかというのも問題のひとつなんですが、これを長くすればするだけ反映される時間は遅くなってしまうし、短くすると今度はSSRと同じになってしまう。キャッシュライフサイクルという問題に移し替えればできますが、本質的には変わらないです。ちなみにこれだけだったらCDNとSSRだけでできてしまうので、ISRだからといってソリューションになっているかというと、ワークアラウンドのひとつでしかないです。

問題1 ―SSR実行時の時間とコスト

(問題1については)いまのところ根本的な解決策はなくて、キャッシュを持つ、もしくはエッジサイドに持っていってなんとかしようとしているフシがある。React 18とNext 12ではStreaming SSRなどの方法もあって、これにより「でき上がったところからちょっとずつ返せばいいんじゃね?」っていう作戦もあるので、その辺もひとつのポイントかなと思います。

いまのところ根本的な解決策はない

Streaming SSRというのは、リクエストが来てから、SSRでHTMLを返すときに全部一気に作って返すんじゃなくて、でき上がったところからちょっとずつ返す感じです。HTMLの描画が開始されてから終わるまでの体感時間を短縮します。

でき上がったところから少しずつ返すStreaming SSR
でき上がったところから少しずつ返すStreaming SSR

問題2 ―JSがロードされるまで「見えるのに押せない!」

JSがロードされるまで操作できない - これは見えるのに押せないという問題があるのですが、これが今のところフレームワーク界隈ではホットトピックだなと思っています。LCP(重要なコンテンツが見えるまで)は速いけど、そこからTTI(操作可能)になる時間が遅いという状況が今生まれています。ここをなんとかしないといけないところです。

問題2 ―見えるのに押せない!」

そもそもbundle.jsのサイズがでかいから削ろうぜ!という話ももちろんあります。たとえば、Next.jsではある程度ページごとやライブラリのサイズやどこから使われているかなどを判断して、最適化したcode split(JavaScriptのコードを削って部分的にしてくれる)をしてくれるということができます。ただ、現時点での最適化はこの状態どまりです。

そもそもbundle.jsのサイズがでかいから

React陣営に関しては、もっと積極的なアプローチを取ろうとしていて、Selective Hydrationを発表しています。2019年のGoogle IOでは、Progressive Hydrationと呼ばれていましたけど、要は部分的に使われているところだけ利用可能にしていくアプローチがあるんじゃない?という話があります。

React陣営Selective Hydrationを発表

すべてのコンポーネントに対して一気にHydrateするのではなくて、段階を踏んで少しずつやっていきましょうというのが、Selective Hydrationの流れになっています。こうすることによって、Hydrationもレイアウトも同じように少しずつでき上がったところからやっていくのが良いんじゃない? というのがReactのアプローチです。

すべてのコンポーネントに対して一気にHydrateするのではなく少しずつ

ここでちょっと待った! です。Reactはこれで良いと思っているとしてもちょっと待った。

ちょっと待った!

そもそもなんでそんな時間がかかるんですか? という話なんですよね。そもそもなんでそんなにHydrationとかをやるのに時間がかかるんですか? そもそものやり方から見直しませんか?

新しいアプローチ ―AstroとQwik

ここで新しいアプローチを2つ紹介します。まず、そもそもなんでそんなに時間がかかるのっていうと、まずJSが重いからですよね。そしてJSがロードされた後にイベントハンドラやサーバで作られた状態をコンポーネントに登録するのに時間がかかる(これをHydrationといいます⁠⁠、この2つが問題になっている。

そもそもなんでそんなに時間がかかるの? まずJSが重い

1つ目の対抗馬がAstroになります。Astroというのは、Next.jsとはまったく別のアプローチで、MPAを基本としています(Next.jsはSPAを基本としています⁠⁠。基本アイデアはReact、Vue、Preactなど何を使ってもいいんですけど、そのかわりサーバサイドでレンダリングするのを基本としていて、クライアントサイドでのJSは基本的には作りません。なので、普通のMPAです。

1つ目の対抗馬 Astro

それじゃインタラクションがあるような部分的な更新が必要なところはどうするの? という話なんですが、インタラクションがあるコンポーネントだけを狙って、そこにだけ「JSロードが必要です」という特別なフラグを置いて、そこだけHydrationすればいいというアプローチです。ほかにもいろいろありますが、これがAstroの基本的なアプローチです。

1つ目の対抗馬 Astro

あとはエッジに置くこともサポートしているので、SSRで遅いという問題もエッジに置いて、分散されたノードでやればCPUの負荷を分散させることができるんじゃない?という話があります。

Astro エッジに置くこともサポート

Astroの解決策は、SSR遅い問題は「Edgeに置けばいい」し、JS遅くてTTI遅い問題は「そもそもJS配信しなければいい」し、イベント登録できなくてという問題は「狙ったところにだけJS読み込ませればいい」というアプローチを取っています。

Astroの解決策

もうひとつの対抗馬のQwikも同じです。QwikはそもそもMPAで、Hydrationなんてそもそもいらないだろという立ち位置です。

もうひとつの対抗馬 Qwik

(Qwikの)言いたいことはこの図に詰まっています。

Qwik

つまりQwikも、基本的にMPAで、Hydrationしないものです。

Qwik 基本的にMPAでHydrationしない

この特徴を以下のようにいろいろ書き換えてみたんですけど、何を言いたいかというと、Reactのコンポーネントを書いて、DOM上にイベントハンドラを読み込ませるためのJSを置きます。イベントが起きたらそのタイミングでそのJSを取ってくるみたいなことをします。

Qwikの特徴
Qwikの特徴

なので、押されるまでJSはロードされてないんです。押されて初めて初期起動していく - かなりオピニオンが強いフレームワークです。

Qwikの解決策

というわけでQwikに関してはいろんなことをやっています。

AstroとQwikはどちらかというとSPAを作ってMPAを作っているところですね。このあたりは動きがあるところなので、続きはディスカッションで話させてください。ありがとうございました。

おすすめ記事

記事・ニュース一覧