2022年12月23日
HTMLを最初に、JavaScriptを最後に:Webを高速化する秘訣
本記事は、原著者の許諾のもとに翻訳・掲載し ております。
すべてのフレームワークはステートを保持する必要があります。フレームワークはテンプレートを実行することでステートを構築します。ほとんどのフレームワークは、このステートをリファレンスやクロージャとしてJavaScriptヒープに保持します。Qwikのユニークな点は、ステートが属性としてDOMに保持されることです(リファレンスもクロージャもシリアライズして送受信するのは不可能ですが、文字列であるDOM属性なら可能です。これがresumability(再開性)のカギとなります)。
DOMにステートを保持することには、以下のように多くのユニークなメリットがあります。
- DOMはシリアライズの形式としてHTMLを使用します。ステートを文字列属性としてDOMに保持することで、アプリケーションをいつでもHTMLにシリアライズできます。HTMLを送信し、別のクライアントでDOMにデシリアライズすることが可能になります。デシリアライズされたDOMは、そこから再開できます。
- 各コンポーネントを他のコンポーネントとは独立して再開できます。このアウトオブオーダー方式の再ハイドレーションによって、アプリケーション全体の一部のみを再ハイドレートすることが可能であり、ユーザのアクションに応じてダウンロードしなければならないコード量が制限されます。これは従来のフレームワークとは全く異なる点です。
- Qwikはステートレスなフレームワークです(アプリケーションのステートはすべて文字列としてDOMに保持されます)。ステートレスなコードはシリアライズ、送受信、再開が簡単です。各コンポーネントを独立に再ハイドレートすることも可能になります。
- アプリケーションを(初回のレンダリング時だけでなく)いつでも何度でもシリアライズできます。
例として、シンプルなCounterコンポーネントと、ステートのシリアライズの仕組みを見てみましょう(これはサーバサイドでレンダリングされたHTMLの出力結果であり、必ずしも開発者がこのようなコードを書くわけではありません)。
<div ::app-state="./AppState"
app-state:1234="{count: 321}">
<div decl:template="./Counter_template"
on:q-render="./Counter_template"
::.="{countStep: 5}"
bind:app-state="state:1234">
<button on:click="./MyComponent_increment">+5</button>
321.
<button on:click="./MyComponent_decrrement">-5</button>
</div>
</div>
::app-state
(アプリケーションステートのコード):アプリケーションステートの変更コードをダウンロードできるURLを指します。ステートの更新コードは、ステートを変更する必要がある場合のみダウンロードされます。app-state:1234
(アプリケーションステートのインスタンス):特定のアプリケーションのインスタンスへのポインタ。ステートをシリアライズすることで、アプリケーションはステートの再構築をやり直すのではなく、中断した所から再開できます。decl:template
(テンプレートの宣言):コンポーネントのテンプレートをダウンロードできるURLを指します。コンポーネントのステートが変更され、再レンダリングする必要があるとQwikが判断するまで、テンプレートはダウンロードされません。on:q-render
(コンポーネントのレンダリングのスケジュール設定):フレームワークは再レンダリングが必要なコンポーネントを追跡しなければなりません。これは通常、無効化されたコンポーネントの内部リストを保存することで行われます。Qwikでは、無効化されたコンポーネントのリストは、属性としてDOMに保存されます。その後、コンポーネントはq-render
イベントのブロードキャストを待ちます。::.="{countStep: 5}"
(コンポーネントのインスタンスの内部ステート):コンポーネントは、再ハイドレーション後も内部ステートを保持しなければならない場合があります。このステートはDOMに保持できます。コンポーネントは、再ハイドレートされた時点で、再開に必要なすべてのステートを保持しています。ステートの再構築は不要です。bind:app-state="state:1234"
(共有アプリケーションステートへのリファレンス):複数のコンポーネントが同じ共有アプリケーションステートを参照できるようにします。
querySelectorAll
は強い味方
フレームワークの一般的な役割の1つは、アプリケーションのステートが変更されたときに、どのコンポーネントの再レンダリングが必要か特定することです。
この作業が発生する理由はいくつかあります。
例えば、コンポーネントが明確に無効とされる場合や(markDirty()
)、アプリケーションの共有ステートの変更によりコンポーネントが暗黙に無効とされる場合などです。
上記の例では、count
がapp-state:1234
をキーとしてアプリケーションステートに保持されています。
ステートが更新された場合、そのアプリケーションステートに依存するコンポーネントは無効化(再レンダリングのキュー)が必要です。
フレームワークはどのように更新すべきコンポーネントを把握するのでしょうか?
ほとんどのフレームワークの場合、その答えは、単純にルートコンポーネントからアプリケーション全体を再レンダリングすることです。 この戦略は、コンポーネントのテンプレートすべてをダウンロードする必要があるという残念な結果を生み、ユーザとのインタラクションのレイテンシに悪影響を及ぼします。
一部のフレームワークはリアクティブで、任意のステートが変更された場合に再レンダリングが必要なコンポーネントを追跡しています。 しかし、これはテンプレートを囲い込むクロージャの形式で記録されます(「クロージャによる死」を参照)。 その結果、リアクティブな接続が初期化されるアプリケーションのブートストラップ時に、すべてのテンプレートをダウンロードしなければなりません。
Qwikはコンポーネントレベルでリアクティブです。 そのため、ルートからレンダリングを開始する必要はありません。 しかし、Qwikはリアクティブなリスナーをクロージャの形式で保持するのではなく、属性の形式でDOMに保持しているので、レンダリングを途中から再開できます。
count
が更新されると、Qwikは以下のquerySelectorAll
を実行し、どのコンポーネントを無効化する必要があるかを内部で判断します。
querySelectorAll('bind\\:app-state\\:1234').forEach(markDirty);
上記のクエリによって、Qwikはどのコンポーネントがステートに依存しているかを判断し、各コンポーネント上でmarkDirty()
を呼び出します。
markDirty()
はコンポーネントを無効化し、そのコンポーネントを再レンダリングが必要なコンポーネントのキューに追加します。
これはmarkDirty
の複数の呼び出しを1つのレンダリングパスに連結することで行われます。
レンダリングパスはrequestAnimationFrame
を利用してスケジュールを設定します。
しかし、ほとんどのフレームワークとは異なり、Qwikはこのキューも属性としてDOMに保持します。
<div on:q-render="./Counter_template" ... >
requestAnimationFrame
はレンダリングのスケジュール設定に利用されます。
これは論理的に考えれば、コンポーネントが待っているq-render
イベントをrequestAnimationFrame
がブロードキャストすることを意味します。
ここで再びquerySelectorAll
の出番です。
querySelectorAll('on\\:q-render').forEach(jsxRender);
ブラウザにはブロードキャストイベント(イベントのバブリングの逆)が存在しませんが、querySelectorAll
を利用すれば、イベントのブロードキャストを受け取るべきコンポーネントをすべて特定できます。
さらに、jsxRender
関数を利用してUIを再レンダリングします。
ポイントは、Qwikがどの時点でもDOMの外部でステートを保持する必要がない点です。 あらゆるステートは属性としてDOMに保持され、自動的にHTMLにシリアライズされます。 つまり、いつでもアプリケーションのスナップショットをHTMLとして保存し、それを送信したり、デシリアライズしたりできるということです。アプリケーションは途中から自動的に再開されます。
Qwikはステートレスであり、それこそがQwikアプリケーションにresumableがある理由です。
メリット
アプリケーションがresumableであることは、フレームワークのステートのすべてをDOMエレメントに保持する明確なメリットです。 しかし、一見しただけでは分かりにくい、他のメリットもあります。
そのメリットとは、ビューポートの外側にあるコンポーネントのレンダリングをスキップできることです。
コンポーネントのレンダリングが必要かを判断するためにq-render
イベントをブロードキャストすると、コンポーネントが表示されているかどうかを判断し、非表示コンポーネントのレンダリングを簡単にスキップできます。
また、レンダリングをスキップすれば、テンプレートなどのコードを一切ダウンロードする必要がありません。
ステートレスのもう1つのメリットは、アプリケーションが実行されている間にHTMLの遅延読み込みが可能であることです。
例えば、サーバは最初に表示される画面のレンダリングに必要なHTMLを送信する一方で、画面に表示されていない部分のHTMLをスキップできます。
ユーザは最初の画面でインタラクションを開始し、アプリケーションを使用することが可能です。
ユーザがスクロールし始めた時点で、アプリケーションは他のHTMLを読み込み、それをDOMの末尾にinnerHTML
で挿入します。
Qwikはステートレスであるため、すでに実行されているアプリケーションに何の問題も発生させずに追加のHTMLを挿入できます。
Qwikが新たなHTMLを認識するのはインタラクションが行われたときであり、その時点までHTMLのハイドレーションは遅延されます。
こうしたユースケースは、現行世代のフレームワークで実現するのがとても難しいものです。
私たちはQwikの未来と、それが切り開くユースケースに大いに期待しています。
- StackBlitzで試す
- github.com/builderio/qwikでスターを送る
- @QwikDev と@builderioをフォロー
- Discordでチャット
- builder.ioの採用情報
この記事はこれで終わりですが、私たちは今後数週間にわたって、Qwikとフロントエンドフレームワークの未来について記事を公開する予定です。お楽しみに。
シリーズ記事一覧
info
シリーズ記事一覧はこちらから参照できます。
株式会社リクルート プロダクト統括本部 プロダクト開発統括室 グループマネジャー 株式会社ニジボックス デベロップメント室 室長 Node.js 日本ユーザーグループ代表
- Twitter: @yosuke_furukawa
- Github: yosuke-furukawa