POSTD PRODUCED BY NIJIBOX

POSTD PRODUCED BY NIJIBOX

ニジボックスが運営する
エンジニアに向けた
キュレーションメディア

POSTD PRODUCED BY NIJIBOX

POSTD PRODUCED BY NIJIBOX

ニジボックスが運営する
エンジニアに向けた
キュレーションメディア

FeedlyRSSTwitterFacebook
Miško Hevery

本記事は、原著者の許諾のもとに翻訳・掲載しております。

すべてのフレームワークはステートを保持する必要があります。フレームワークはテンプレートを実行することでステートを構築します。ほとんどのフレームワークは、このステートをリファレンスやクロージャとしてJavaScriptヒープに保持します。Qwikのユニークな点は、ステートが属性としてDOMに保持されることです(リファレンスもクロージャもシリアライズして送受信するのは不可能ですが、文字列であるDOM属性なら可能です。これがresumability(再開性)のカギとなります)。

DOMにステートを保持することには、以下のように多くのユニークなメリットがあります。

  1. DOMはシリアライズの形式としてHTMLを使用します。ステートを文字列属性としてDOMに保持することで、アプリケーションをいつでもHTMLにシリアライズできます。HTMLを送信し、別のクライアントでDOMにデシリアライズすることが可能になります。デシリアライズされたDOMは、そこから再開できます。
  2. 各コンポーネントを他のコンポーネントとは独立して再開できます。このアウトオブオーダー方式の再ハイドレーションによって、アプリケーション全体の一部のみを再ハイドレートすることが可能であり、ユーザのアクションに応じてダウンロードしなければならないコード量が制限されます。これは従来のフレームワークとは全く異なる点です。
  3. Qwikはステートレスなフレームワークです(アプリケーションのステートはすべて文字列としてDOMに保持されます)。ステートレスなコードはシリアライズ、送受信、再開が簡単です。各コンポーネントを独立に再ハイドレートすることも可能になります。
  4. アプリケーションを(初回のレンダリング時だけでなく)いつでも何度でもシリアライズできます。

例として、シンプルな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())、アプリケーションの共有ステートの変更によりコンポーネントが暗黙に無効とされる場合などです。

上記の例では、countapp-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の未来と、それが切り開くユースケースに大いに期待しています。

この記事はこれで終わりですが、私たちは今後数週間にわたって、Qwikとフロントエンドフレームワークの未来について記事を公開する予定です。お楽しみに。

シリーズ記事一覧

info

シリーズ記事一覧はこちらから参照できます。

監修者
監修者_古川陽介
古川陽介
株式会社リクルート プロダクト統括本部 プロダクト開発統括室 グループマネジャー 株式会社ニジボックス デベロップメント室 室長 Node.js 日本ユーザーグループ代表
複合機メーカー、ゲーム会社を経て、2016年に株式会社リクルートテクノロジーズ(現リクルート)入社。 現在はAPソリューショングループのマネジャーとしてアプリ基盤の改善や運用、各種開発支援ツールの開発、またテックリードとしてエンジニアチームの支援や育成までを担う。 2019年より株式会社ニジボックスを兼務し、室長としてエンジニア育成基盤の設計、技術指南も遂行。 Node.js 日本ユーザーグループの代表を務め、Node学園祭などを主宰。