POSTD PRODUCED BY NIJIBOX

POSTD PRODUCED BY NIJIBOX

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

POSTD PRODUCED BY NIJIBOX

POSTD PRODUCED BY NIJIBOX

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

FeedlyRSSTwitterFacebook
Miško Hevery

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

Qwikは、JavaScriptの読み込みと実行を可能な限り遅延させ、ユーザのアクションがあった場合のみ実施することで、読み込みを最も高速化することを目指しています。 この処理は初回の読み込み時だけでなく、アプリケーションが続く限り行われます。 言い換えると、Qwikはきめ細かい遅延読み込みを追求しているのです。 「きめ細かい」とは、ユーザのアクションを処理するのに直接必要なコードのみがダウンロードされるという意味です。 この記事では、きめ細かい遅延読み込みを実現するために解決すべき技術的課題について探っていきます。

リスナーをシリアライズ

最も明確に解決すべき課題は初回のページ読み込みです。 この点に関しては、「HTMLを最初に、JavaScriptを最後に」で対応策をすでに取り上げました。 ポイントは、イベントの名称とアクションをURLとしてシリアライズし、DOMの属性として保持することです。その後、最上位のグローバルなイベントハンドラがイベントをリッスンし、そのイベントに関連するコードをダウンロードできます。

<button on:click="./MyComponent_onClick">click me</button>

このコードは、初回のページ読み込みで(1KBのローダを除いて)JavaScriptを一切読み込むことなく、上記の処理を実現します。 これにより初回読み込み時のTime to Interactiveの目標を達成できますが、新たな問題が生まれます。 私たちは、ユーザとの初回のインタラクション時にアプリケーション全体をダウンロードしたり、ブートストラップしたりしたくはありません。 それでは初回の読み込み時から初回のインタラクション時へ問題を先送りするだけになってしまいます(それどころか、ユーザとの初回のインタラクション時に大きなレイテンシが生じるので、状況は悪化しています)。

この問題は、ユーザとの1回のインタラクションで、アプリケーション全体のダウンロードやブートストラップが行われないようにすれば解決です。 その代わり、インタラクションの処理に直接必要なコードやコンポーネントのみのダウンロード、ブートストラップ、再ハイドレーションを行えばよいのです。 そのため、きめ細かい遅延読み込みが必要になります。

これはイベントをHTMLやDOMへシリアライズすればすべて実現できます。 そうしなければ、テンプレートの読み込みを遅延させるのは不可能でしょう。 なぜなら、フレームワークはイベントの場所を特定するためにテンプレートをダウンロードする必要があるからです。

非同期かつアウトオブオーダー方式によるコンポーネントのハイドレーション

初回のインタラクションによるアプリケーション全体のダウンロードやブートストラップを避けるには、コンポーネントを非同期かつアウトオブオーダー方式で再ハイドレートする必要があります。

ここでいう非同期とは、レンダリングシステムがレンダリングを一時停止してコンポーネントのテンプレートを非同期にダウンロードし、それからレンダリングのプロセスを再開できるという意味です。 レンダリングプロセスがすべて完全な同期方式である既存のフレームワークとはとても対照的です。 レンダリングが同期方式である場合、非同期の遅延読み込みを挿入する余地はありません。その結果、すべてのテンプレートをレンダリングの呼び出し前に準備する必要があります。

既存の再ハイドレーション戦略にはもう1つ問題があります。 それは再ハイドレーションがルートコンポーネントから始まり、ルート以下のすべてのコンポーネントを同期方式で再ハイドレートすることです。 そのため、すべてのコンポーネントを同時に再ハイドレートしなければならず、あらゆるコンポーネントのダウンロードを強いられます。 その結果、初回のインタラクション時の処理時間が長くなります。 アウトオブオーダー方式のハイドレーションとは、各コンポーネントを他のコンポーネントとは独立して任意の順番で再ハイドレートできるという意味です。 これにより、Qwikはリクエストの処理に必要な最小限のコンポーネントのみを再ハイドレートすることが可能になります。

<div decl:template="./path/MyComponent_template">
  ... some content ...
</div>

上記のケースでは、<div>MyComponent_template.tsと紐づけられたコンポーネントを表しています。 Qwikは、コンポーネントを再レンダリングする必要があると判断した場合のみテンプレートをダウンロードするため、ダウンロードがさらに遅延されます。

再ハイドレーションがアウトオブオーダー方式でなければ、フレームワークはすべてのテンプレートを一度にダウンロードし、再ハイドレートしなければなりません。 その結果、初回のインタラクション時に、ダウンロードと実行による大きな負荷がかかります。

レンダリングをイベントハンドラから分離

Qwikに関して考慮が欠かせないポイントは、既存のあらゆるレンダリングシステムがイベントリスナーをテンプレートに埋め込んでいることです。 そのため、コンポーネントを再レンダリング(または再ハイドレート)する必要があるときに、ブラウザはすべてのリスナーを必要性にかかわらずダウンロードしなければなりません。 リスナーは複雑なコードに紐づいていることが多いので、ダウンロードされるコード量はさらに増加します。

import {complexFunction} from './large-dependency';

export function MyComponent() {
  return (
    <button onclick={() => complexFunction()}>
      rarely clicked => click handler downloaded eagerly
    </button>
  );
}

Qwikはイベントハンドラをテンプレートのコードから分離します。これはリスナーとテンプレートを別々に、必要に応じてダウンロードできることを意味します。

MyComponent_template.ts

export MyComponent_template() {
  return (
    <button on:click="./MyComponent_onClick">
      rarely clicked => click handler downloaded lazily
    </button>
  );
}

MyComponent_onClick.ts

import {complexFunction} from './large-dependency';

export default function() {
  complexFunction();
}

イベントハンドラをテンプレートから分離しなければ、フレームワークはコンポーネントの再レンダリングに必要な量よりはるかに多くのコードをダウンロードしなければなりません。 さらに、イベントハンドラは複雑で、他の依存関係を持っていることが多いため、ダウンロードが必要なコード量は増加します。

コンポーネントのステートのシリアライズ

コンポーネントを再ハイドレートするプロセスの最も重要な部分は、コンポーネントのステートを復元することです。 既存のフレームワークにはステートをシリアライズする方法がありません。 コンポーネントのステートがどこにあるかを確定するための標準的な方法が存在しないからです。

Qwikはコンポーネントをいくつかの部分に分解します。

  • props:コンポーネントの単なるプロパティ。DOMに反映される。例えば<counter min="0" max="100"/>のpropsは{min: 0, max: 100}
  • state:コンポーネントの内部ステート。DOMにシリアライズできる。
  • transient state:追加的なステートで、コンポーネントによるキャッシュは可能だが、シリアライズは不可能なものを指す。この情報は再計算する必要がある(例:コンポーネントとサーバが通信している間の一時的なプロミス)。
<div decl:template="./Conter_template"
     :.='{count: 42}'
     min="0" max="100">
  <button on:click="./Counter_incremente">+</button>
  42
  <button on:click="./Counter_decrement">+</button>
</div>

コンポーネントがステートをシリアライズできない場合、特定のコンポーネントを独立に再ハイドレートするのは不可能でしょう(コンポーネントはどこでステートを取得するというのでしょうか?)。 その結果、フレームワークは、ステートの計算やダウンロードのための追加コードをサーバからダウンロードしなければなりません。 Qwikはこうした問題のすべてをDOM内にステートをシリアライズすることで回避します。

アプリや共有ステートのシリアライズ

コンポーネント内のみで有効なコンポーネントステートに加え、複数のコンポーネントで利用されるアプリケーションステートも存在します。 これもDOM内にシリアライズする必要があります。共有ステートは以下に分解できます。

  • key:あるステートをただ1つに特定するID。コンポーネント内でステートを参照するために利用される。
  • state:複数のコンポーネントで共有されるステート。DOM内にシリアライズできる。
  • transient state:アプリケーションによるキャッシュは可能だが、シリアライズは不可能な追加ステート。この情報は再計算が可能でなければならない。
<div :cart:="./Cart"
     cart:432="{items: ['item:789', 'item:987']}"
     :item:="./Item"
     item:789="{name: 'Shoe' price: '43.21'}"
     item:987="{name: 'Sock' price: '12.34'}">
  ...
</div>

アプリケーションのステートをシリアライズすることで、コンポーネントが同じ情報を複数の場所でレンダリングし、他のコンポーネントとコミュニケーションできます。 フレームワークが共有ステートを把握・管理しないと、フレームワークがステートの変更を認識できないため、コンポーネントの独立したハイドレーションが不可能になります(例えば、AngularとReactにはレンダリング関数に紐づけられた明確なステート管理機能がありません。その結果、アプリケーションのステートが変更されたときにアプリケーション全体を再レンダリングする以外に妥当な方法がなく、きめ細かい遅延読み込みが困難です)。

アプリのステートとコンポーネントの間のリアクティブな関係

ステートを把握するフレームワークの真のメリットは、フレームワークがステートとコンポーネントの関係を認識できる点にあります。 これが重要なのは、任意のステートが変化したときに、フレームワークがどのコンポーネントを再ハイドレートする必要があるか把握できるためです。 あるいは、それ以上に重要なのは、ステートが変化したときに、フレームワークがどのコンポーネントを再ハイドレートする必要がないかを把握できることでしょう。 例えば、ショッピングカートに商品を追加するときは、ショッピングカート内の商品の個数を表示するコンポーネントのみを再レンダリングすべきですが、これはページ全体のごく一部でしかありません。

<div :cart:="./Cart"
     cart:432="{items: ['item:789', 'item:987']}">
  <div decl:template="./Unrelated">...</div>
  <div decl:template="./ShoppingCart"
       bind:cart:432="$cart">
   2 items
  </div>
  <button on:click="./AddItem">buy</button>
</div>

Qwikの目標は再ハイドレートするコンポーネントの数を最小限にすることです。 ユーザが<button>をクリックすると、Qwikは./AddItemをダウンロードし、cart:432のアプリケーションステートを更新します。 さらにQwikは、bind:cart:432を持つコンポーネントが、そのステートを利用している唯一のコンポーネントであり、したがって再ハイドレーションと再レンダリングが必要な唯一のコンポーネントであると判断します。 Qwikはページ上のほとんどのコンポーネントを除外できるため、きめ細かい遅延読み込みを維持できます。 どのコンポーネントがどのステートと関係しているかを把握できるという点は、他のフレームワークには存在しないとても重要な特徴です。 この特徴によって、アプリケーションの起動時やライフサイクル全体を通じたきめ細かい遅延読み込みが可能になります。

コンポーネントの分離

これまでQwikがどのようにコードのきめ細かい遅延読み込みをサポートするか説明してきました。 上記の仕組みがすべて機能するのは、Qwikがアプリケーション内のデータフローを認識しているからです。 Qwikはこの情報を、再ハイドレートする必要がないコンポーネントを取り除き、必要なコンポーネントのみを再ハイドレートするために利用します。 これが暗に意味するのは、Qwikがコンポーネントと他のコンポーネントの通信を認識する必要があるということです。 コンポーネントは他のコンポーネントと秘密の会話をしてはならないのです。

コンポーネントがステートを取得したことをQwikが把握できない場合、Qwikは、ステートが変更されたときにそのコンポーネントの再ハイドレーションや再レンダリングが必要であることを認識できません。 そのため、コンポーネントは自身の依存関係のリストをプロパティに明記する必要があります。

この明確なリストがないと、フレームワークは、ステートが変更された時点ですべてを再レンダリングしなければなりません。 その結果、アプリケーション全体のダウンロードとブートストラップが行われることになります。

結論

Webアプリケーションが遅延読み込みに対応した構造となるように、開発のアプローチを変える必要がある点は数多くあります。 ポイントは、現在のフレームワークはこの問題を解決する助けにならず、ときには悪化させるという点です(例えば、ページ全体の再ハイドレーション、同期方式のレンダリングなど)。 Qwikはきめ細かい遅延読み込みを実現し、どんなに大規模で複雑であっても1秒未満で読み込めるサイトやアプリを開発できます。

シリーズ記事一覧

info

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

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