2022年11月24日
クロージャによる死(とQwikによる解決方法)
本記事は、原著者の許諾のもとに翻訳・掲載しております。
世界中にQwikを紹介した前回の記事では、 多くの要素についてざっと触れるに留まり、詳細については後で説明するとお約束していました。 Qwikとその背景にある設計思想を知る前に、まず私たち(業界)が現在の場所までどのようにたどり着いたかを理解しておくことが重要です。 現代のフレームワークはある前提のもとに成り立っており、 それが優れたTime to Interactive(TTI)スコアの実現を妨げているのですが、 その前提とはどのようなものなのでしょうか。 現行世代のフレームワークの現時点における限界を理解することで、 Qwikの設計思想がなぜ最初は驚くべきものに思えるのか、より深く知ることができるでしょう。
TTIについて
TTIは、URLに遷移してからページがインタラクティブになるまでの時間を測定したものです。 レスポンシブなサイトとしての体裁を整えるには、サーバーサイドレンダリング(SSR)が必須です。 この背景にある考え方は、サイトを素早く表示し、ユーザがクリックすればいいか判断できるようになるまでに、 アプリケーションが自身をブートし、すべてのリスナーをインストールするというものです。 そのためTTIは、実際にはフレームワークがDOMリスナーをインストールするのにかかる時間を測定していると言えます。
上の図では、ブートからインタラクティブになるまでの時間に焦点を当てています。 フレームワークがインタラクティブな状態に達するまでに実行すべきことすべてを理解できるよう、 インタラクティブになった時点からスタートして逆にたどってみましょう。
- フレームワークはリスナーがどこにあるかを知る必要があります。しかし、この情報はフレームワークが簡単に利用できるものではありません。リスナーはテンプレートに
described
(記述)されています。 - 実は、筆者は
described
よりもembedded
(埋め込み)という言葉のほうが適切だと考えています。情報が埋め込まれていると言えるのは、フレームワークが情報を簡単に利用できないからです。フレームワークは、リスナーのクロージャに到達するためにテンプレートを実行する必要があります。 - テンプレートを実行するにはダウンロードしなければなりません。しかし、ダウンロードされたテンプレートには import が含まれており、これを読み込むためにさらに多くのコードのダウンロードが必要です。テンプレートはそのサブテンプレートをダウンロードする必要があります。
- テンプレートの準備はできましたが、依然としてリスナーには到達していません。テンプレートの実行は、実際にはテンプレートとステートをマージすることを意味します。ステートがなければ、フレームワークはテンプレートを実行できず、リスナーにはたどり着けません。
- ステートはダウンロードしたり、クライアント側で計算したりする必要があります。頻繁に計算をするということは、ステートを計算するためにさらに多くのコードをダウンロードする必要があることを意味します。
すべてのコードがダウンロードされると、フレームワークはステートを計算し、そのステートをテンプレートにフィードすることが可能になります。 そして、ようやくリスナーのクロージャにたどり着き、クロージャをDOMにインストールできます。
インタラクティブなステートに至るには多くの作業が必要です。 現行世代のあらゆるフレームワークはこのように動作しています。 つまり、フレームワークがリスナーを発見してインストールするには、 結局はアプリケーションの大部分をダウンロードして実行しなければなりません。
クロージャについて
上記の問題の核は、コードをダウンロードするには大きな処理能力が必要であること、 そしてフレームワークがリスナーを発見し、ページがインタラクティブな状態になるまでに長いCPU時間がかかることです。 しかし、私たちはクロージャがコードとデータを囲い込むという性質を忘れています。 これはとても便利な特性であり、私たちがクロージャを気に入っている理由です。 しかし、これは同時にクロージャのすべてのデータとコードが、 クロージャの実行時に遅延して作成されるのではなく、 クロージャを作成した時点で利用可能でなければならないことも意味します。
例としてシンプルなJSXテンプレートを見てみましょう(ただし、他のテンプレートシステムにも同じ問題があります)。
import {addToCart} from './cart';
function MyBuyButton(props) {
const [cost] = useState(...);
return (
Price: {cost}
<button onclick={() => addToCart()}>
Add to cart
</button>
);
}
クロージャによる死
クロージャはコストが低いため、至るところで見られます。 しかし、クロージャは本当に低コストなのでしょうか。 その答えはイエスでもあり、ノーでもあります。 確かに、ランタイムにおいて短時間で作成できるという意味では低コストです。 しかし、コードを囲い込むために、クロージャがない場合より大幅に早くコードをダウンロードする必要があるという点では高コストです。 さらに、ツリーシェイキングを妨害するという意味でも高コストです。 そのため、筆者が「クロージャによる死」と呼ぶ状況が起きます。 クロージャはDOMに設置されるリスナーであり、実行される可能性がほとんどないコードを囲い込みます。
ページの「購入する」ボタンは複雑な割にしょっちゅうクリックされるようなものではありません。 それでも「購入する」ボタンは、関連するすべてのコードのダウンロードをしきりに促します。 これはクロージャがそのような仕組みになっているためです。
QwikはリスナーのHTMLシリアライズを可能にする
これまで、クロージャには隠れたコストがあるかもしれないと説明してきました。 こうしたコストはコードの頻繁なダウンロードという形で生じます。そのため、クロージャを作成し、ユーザとインタラクティブなWebサイトの間に介在させるのは困難です。
Qwikはリスナーの作成を可能な限り遅らせようとします。 これを実現するために、Qwikは以下のルールを掲げています。
- リスナーはHTMLにシリアライズできなければならない。
- ユーザがリスナーとのインタラクションを行うまで、リスナーはコードを囲い込まない。
これがどのように実現されるのか、実例で見てみましょう。
<button on:click="MyComponent_click">Click me!</button>
MyComponent_click.ts
ファイルの内容は以下のとおりです。
export default function () {
alert('Clicked');
}
上記のコードをご覧ください。 SSRはレンダリングプロセスの間にリスナーの場所を発見した後、 その情報を捨てるのではなく、リスナーを属性としてHTMLにシリアライズします。 そのため、クライアントはリスナーの場所を発見するためにテンプレートを再実行する必要がありません。 その代わりとして、Qwikは以下のアプローチをとります。
qwikloader.js
をページにインストールする。このファイルのサイズは1KB未満で、実行には1ミリ秒もかからない。ファイルがとても小さくHTMLに埋め込むのが最善の方法であるため、サーバーとのラウンドトリップが不要になる。qwikloader.js
は、1つのグローバルなイベントハンドラを登録し、バブリングを活用することで、すべてのイベントを同時にリッスンできる。addEventListenerの呼び出しが少なくなるため、インタラクティブな状態までの時間が短くなる。
その結果が以下のとおりです。
- リスナーを発見するためにテンプレートをダウンロードする必要がない。リスナーは属性としてHTMLにシリアライズされる。
- リスナーを取得するためにテンプレートを実行する必要がない。
- テンプレートを実行するためにステートをダウンロードする必要がない。
- すべてのコードが遅延され、ユーザがリスナーとのインタラクションを行うときにのみダウンロードされる。
Qwikは現行世代のフレームワークによるブートストラップのプロセスを省略し、1つのグローバルなイベントリスナーで代替します。 この方法が最も優れているのは、アプリケーションのサイズに影響されないところです。 どんなにアプリが大規模で複雑になってもリスナーは1つだけです。 すべての情報がHTMLにシリアライズされているため、ダウンロードすべきブートストラップコードは常に一定で、その容量はアプリケーションの複雑性と無関係です。
まとめると、Qwikの背景にある基本的な考え方は再開性(resumability)です。 Qwikはサーバーの作業が中断された時点から作業を再開し、クライアント側が実行する必要があるファイルはわずか1KBです。 このコードはアプリケーションがどんなに大規模で複雑になっても変わりません。 これから数週間(訳注:原文では定期的に8記事まで公開されています)にわたって、 Qwikがどのように作業を再開し、ステートを管理し、コンポーネントを独立にレンダリングするかを見ていきます。お楽しみに。
私たちはQwikの未来や、Qwikが実現する新たな種類のユースケースに大いに期待しています。
- StackBlitzで試す
- github.com/builderio/qwikでスターを送る
- @QwikDev と@builderioをフォロー
- Discordでチャット
- builder.ioの採用情報
シリーズ記事一覧
info
シリーズ記事一覧はこちらから参照できます。
株式会社リクルート プロダクト統括本部 プロダクト開発統括室 グループマネジャー 株式会社ニジボックス デベロップメント室 室長 Node.js 日本ユーザーグループ代表
- Twitter: @yosuke_furukawa
- Github: yosuke-furukawa