# Resumable(Qwik)
Resumableとは、フロントエンドフレームワークのQwik (opens new window)のキーコンセプトの一つで、ハイドレーションの課題を解決するために考案されたアーキテクチャです。ハイドレーションの処理を無くして、パフォーマンスを最大限に最適化するという目的で設計されています。
ハイドレーションの課題を解決するというコンセプトは、React18のSelective Hydrationや、AstroやFreshといったIslands Architectureでも進められています。これらは、ハイドレーションの実行を非同期化して遅延させる、または、部分的に行うというアプローチになります。
Resumableでは、ハイドレーション自体を無くして、サーバサイドとクライアントサイドの状態をシームレスに繋ぐというアプローチになります。そのため、ハイドレーションが伴うフレームワークよりも、パフォーマンス性に特化しているアーキテクチャになります。
# ハイドレーションの問題点
Resumableのコンセプトを理解するため、ハイドレーションの課題について見てみましょう。
SSRでは、サーバサイドでJavaScriptを実行して、コンテンツを生成し、クライアントへ配信します。クライアントでは、レンダリングする必要がないため、コンテンツをそのまま表示することができます。しかし、ボタンをクリックしたりなどのインタラクティブな操作が機能しません。なぜなら、DOMにイベントがバイディングされていないためです。このインタラクティブな操作を実現するには、JavaScriptをダウンロードして、実行する必要があります。この処理をハイドレーションと呼びます。
一般的に、ハイドレーションは、次のような処理をしています。
- コンポーネントツリーの復元
- イベントのバインディング
- サーバサイドで生成されたアプリケーションの状態を復元
レンダリングの処理は、サーバサイドですでに実行されていますが、クライアントでも、再度、コンポーネントツリーを生成して、アプリケーションの整合性を保つ必要があります。レンダリングが発生しない分、CSRよりも低コストで済みますが、JavaScriptをダウンロードして、実行する必要があるため、依然としてパフォーマンス性の課題が残ります。 Next.jsでは、ページ内のほとんどが静的コンテンツで、インタラクティブな動作を必要としていなくても、ハイドレーションは発生してしまいます。
このような、一連のハイドレーションの処理が、パフォーマンスに影響を及ぼし、アプリケーションが機能するまでの時間(TTI)が遅くなるという問題が発生します。
Resumableは、そのような課題を解決するため、ハイドレーションを排除して、アプリケーションの整合性を保つように設計されています。
出典: Resumable (opens new window)
JavaScriptをダウンロードして、実行するという点は変わりませんが、それをユーザが操作してからシームレスに行うというアプローチをとっています。具体的な仕組みは、次の章で見てみましょう。
# Resumableの仕組み
Resumableは、ハイドレーションをせずにインタラクティブな動作を実現します。ハイドレーションが必要ないため、初回の起動時に、オーバーヘッドなく、アプリケーションを機能させることができます。
具体的には、サーバサイドでレンダリングしたアプリケーションの状態を、クライアントで必要なタイミングで復元できるように設計されています。例えば、カウンターアプリのような場合、カウンターのボタンを押したタイミングで、必要なJavaScriptコードがダウンロードされ、実行されます。そのため、アプリケーション起動時には、ハイドレーションをする必要がなく、JavaScriptのダウンロードも必要ありません。
Qwikは、上記のフローを、サーバーサイドの処理を「停止」し、クライアント側で「再開する」というような表現をしています。Resumableの名前が示す通り、サーバーサイドの状態をクライアント側でシームレスに再開するという意味になります。
まとめると、Resumableのステップは次のようになります。
- サーバサイドでHTMLを生成する
- アプリケーションの状態をHTMLにシリアライズする
- クライアントでアプリケーションが起動
- ユーザーが操作したタイミングで、JavaScriptがダウンロードされ、実行される
具体的に、上記がどのように実現されるのか見てみましょう。
# Listeners
一般的なハイドレーションは、アプリケーション起動時に、コンポーネントツリーを復元して、インタラクティブな操作を含む箇所にイベントリスナーを登録します。そのため、初回起動時にはJavaScriptのダウンロードが必要となります。
しかし、Resumableの場合、アプリケーション起動時にはJavaScriptのダウンロードは必要ありません。ユーザが操作したタイミングで初めて、必要なJavaScriptがダウンロードされます。これを実現するために、Resumableでは、次のような要素をHTMLに記述しています。
<button q:id="2" on:click="app_component_main_p_button_onclick_26jvv9xghyk.js#app_component_main_p_button_onClick_26jvV9XGhyk[0]">
Click
</button>
<button>
に、on:click
という要素が記述されています。値には、JavaScriptファイルが指定されています。これがシリアライズされた情報で、ユーザがクリックしたタイミングで指定のJavaScriptがダウンロードされ、実行されます。
実際に、Qwikのデモを見てみましょう。
デモ: Counter (opens new window)
カウンターアプリで、次のようなコンポーネントが定義されています。
import { component$, useStore } from '@builder.io/qwik';
export default component$(() => {
const store = useStore({ count: 0 });
return (
<main>
<p>Count: {store.count}</p>
<p>
<button onClick$={() => store.count++}>Click</button>
</p>
</main>
);
});
そして、ボタンを押下したときに次のようなファイルがダウンロードされているのが分かります。
import { b as useLexicalScope } from './app2.js';
const app_component_main_p_button_onClick_26jvV9XGhyk = ()=>{
const [store] = useLexicalScope();
return store.count++;
};
export { app_component_main_p_button_onClick_26jvV9XGhyk };
このダウンロードされたファイルが<button>
に適用されて、カウンターボタンが機能するようになっています。
では、<button>
がクリックされたときに、どのようにJavaScriptファイルをダウンロードしているのでしょうか。
シリアライズした情報からJavaScriptをダウンロードするためには、Qwikloader (opens new window)という仕組みが使われています。具体的には、グローバルなリスナーを登録しておき、イベントが発生したタイミングでインターセプトし、指定されたJavaScriptファイルをダウンロードするというものです。
Qwikloaderは、HTMLのscriptタグに埋め込まれているので、ダウンロードする必要はありません。例えば、次のように、HTMLと一緒にクライアントへ配信されます。
<html>
<body q:base="/build/">
<button on:click="./myHandler.js#clickHandler">push me</button>
<script>
/* Qwikloader */
</script>
</body>
</html>
アプリケーション起動時に、Qwikloaderが読み込まれて、グローバルイベントリスナーが登録されます。あとは、ユーザーが操作するたびに都度、必要な分のJavaScriptがダウンロードされ、適用されます。そのため、ハイドレーション無しで、インタラクティブな操作が可能となっています。
# Prefetching
ユーザーが操作するたびにJavaScriptをダウンロードするとなると、クリックしたときの動作が遅れる可能性があります。Qwikでは、この問題に対してPrefetching (opens new window)という仕組みが使われています。
Prefetchingは、事前にJavaScriptをダウンロードしておいて、パフォーマンスを最適化するための仕組みです。<link rel>
によるprefetch、Web Workerによるキャッシュ、Service Workerによるキャッシュ、などが選択肢としてあります。
出典: Prefetching (opens new window)
推奨されているのはService Workerによるキャッシュを用いた方法 (opens new window)で、仕組みとしては、Service Workerが事前に使われる可能性のあるコンポーネントのJavaScriptをバックグラウンドで取得し、キャッシュします。これにより、ユーザが操作したときにはすでにダウンロードされた状態になるため、タイムラグがなく実行することができます。Prefetchingは、メインスレッドでは行われないため、アプリケーションに影響を与えることもありません。
実際に、ダウンロードされたファイルを見ると、Service Workerから取得されているのが分かります。
このような最適化が行われているため、クライアントでのインタラクティブな動作がシームレスに実現することができます。
# Component Tree
一般的に、コンポーネントを再レンダリングする際は、変更があった箇所だけを特定して更新します。ハイドレーションでは、変更があった箇所を特定するために、クライアント側で全てのコンポーネントツリーを再度構築する必要があります。そのため、アプリケーションの規模が大きくなるにつれ、オーバーヘッドが大きくなる可能性があります。
Resumableでは、サーバーサイドでレンダリングするときに、コンポーネントツリーの情報を収集し、HTMLにシリアライズして添付します。クライアントでは、その情報をもとに変更があったコンポーネントだけを更新することができます。そのため、アプリケーションの規模が大きくなったとしても、必要なコンポーネントや処理だけをダウンロードし実行するので、オーバーヘッドが発生しにくい設計になっています。
具体的に、どのようにコンポーネントの特定をしているかというと、Qwikでは、$ (opens new window)サインを使ったコンポーネントやイベントに対して、Optimizerで細かくチャンクしています。例えば、コンポーネントではcomponent$
が使われます。
import { component$ } from '@builder.io/qwik';
export default component$(() => {
return <div>Hello World!</div>;
});
次のように、複数のコンポーネントが使われている場合も、それぞれがチャンクされるようになっています。
import { component$ } from '@builder.io/qwik';
export default component$(() => {
return (
<>
<p>Parent Text</p>
<Child />
</>
);
});
const Child = component$(() => {
return <p>Child Text</p>;
});
チャンクされる対象はコンポーネントだけでなく、イベントハンドラに対しても行われます。例えば、onClick$
はクリックイベントハンドラーに関する処理をチャンクして遅延実行することができます。
import { component$ } from '@builder.io/qwik';
export default component$(() => {
console.log('render');
return <button onClick$={() => console.log('hello')}>Hello Qwik</button>;
});
上記の処理が実際にチャンクされると、次のようなファイルに分割されます。
ルートとなるファイル:
// app.js
import { componentQrl, qrl } from '@builder.io/qwik';
const App = /*#__PURE__*/ componentQrl(
qrl(() => import('./app_component_akbu84a8zes.js'), 'App_component_AkbU84a8zes')
);
export { App };
コンポーネントのファイル:
// app_component_akbu84a8zes.js
import { jsx as _jsx } from '@builder.io/qwik/jsx-runtime';
import { qrl } from '@builder.io/qwik';
export const App_component_AkbU84a8zes = () => {
console.log('render');
return /*#__PURE__*/ _jsx('p', {
onClick$: qrl(
() => import('./app_component_p_onclick_01pegc10cpw'),
'App_component_p_onClick_01pEgC10cpw'
),
children: 'Hello Qwik',
});
};
イベントハンドラの処理のファイル:
// app_component_p_onclick_01pegc10cpw.js
export const App_component_p_onClick_01pEgC10cpw = () => console.log('hello');
その他にも、Tasks (opens new window)と呼ばれる処理や、ライフサイクルも細かくチャンクすることができます。$
サインを使わなければならないという制約はありますが、コンポーネントを細かいチャンクにし、それぞれの境界線を把握することで、特定のコンポーネントのレンダリングが可能となっています。ハイドレーションが全てのコンポーネントツリーを再生成するのに対して、Resumableでは、レンダリングのコストを最小限にできる設計になっています。
# メリット
# パフォーマンス性が高い
Resumableでは、ハイドレーションがありません。代わりに、コンポーネントやイベントハンドラ、タスクなどの処理を細かくチャンクにして、必要なタイミングで実行できるように最適化されています。そのため、クライアントで起動時のオーバーヘッドがなく、アプリケーションが機能するまでの時間が圧倒的に早くなります。 また、アプリケーション全体のJavaScriptをダウンロードする必要もないため、メモリの節約にもなります。スペックの低いモバイルデバイスや、低ネットワークの環境では、特に顕著に違いが出るでしょう。
# デメリット
# ユースケースは限られる
インタラクティブな動作が多いシングルページアプリケーションや、パフォーマンス性をそこまで要求されないWebアプリケーションの場合、Resumableを採用するメリットは低いかもしれません。Resumableはパフォーマンスに特化したアーキテクチャなので、要件に合わせて選定する必要があるでしょう。
# フレームワーク依存になる
Resumableを実現できるフレームワークは限られており、Qwikのみが対応しています。そのため、$
サインや、独自のAPIを使う必要があり、フレームワークの仕様に依存する形になります。コミュニティやエコシステムはまだ発展途中で、ベストプラクティスなどのパターンも定められていないため、チームでよく検討しながら開発していく必要があるでしょう。
ただ、ハイドレーションの課題はフロントエンドの共通の認識なので、今後、Resumableのようなアプローチが主流になる可能性もあります。フレームワークの使い方よりも、アーキテクチャの仕組みや、考え方、コンセプト、どのようにパフォーマンスが最適化されているか、などを知ることが重要になります。