最近調べた qwik というライブラリが結構面白かったので、実際どういうものなのかとか紹介してみます。
qwik とは
qwik は Web 向けの View ライブラリです (React や Vue.js の仲間)。パフォーマンスオタクがパフォーマンスの最適化 (Web Vitals の改善) にこだわって作ったライブラリです *1。
すでにいくつも良い紹介資料があるので、まずはこれらをいくつか読んでみると良いと思います。
- Resumable な JavaScript フレームワーク Qwik を学ぶ
- Qwikの基本概念である Resumable を理解する
- Qwikというフレームワークについて - console.lealog();
- Qwik調べてみたら結構面白かった
qwik の詳しい使い方などは先人の記事に譲ることにして、以降は id:mizdra が個人的に面白いと思ったことを書いていきます。
Hydration の問題点
最近は SEO であったりページの初期表示に掛かる時間 (FP/FCP/LCP など) の改善のために、SSR をするのが一般的になっていると思います。SSR では、ユーザからリクエストを受け取った時、サーバー上で React コンポーネントツリーをレンダリングして HTML へとシリアライズし、それがレスポンスとして返されます。HTML 中に最初からコンテンツが描画されているため、CSR と比較すると、コンテンツを早いタイミングでユーザに表示できます。
しかし、@aiji42 さんの Qwikの基本概念である Resumable を理解する で語られているように、SSR には Hydration という処理をどうしても挟む必要があります。ページ中に存在する全てのコンポーネントのソースコードを DL して、それを実行して、(onClick プロパティなどで渡された) イベントリスナーを DOM に登録していきます。コンポーネントのソースコードの DL や評価、などなど様々な計算が発生し、ユーザの貴重な計算リソースを奪ってしまいます。特にこうした計算は、計算リソースがシビアなページの表示の初期に発生するため、FP/FCP/LCP といった Web Vitals に強い影響を与えます。
イベントリスナーが設定されないと、ユーザがクリックしてページに変化を起こすことはできないですし、コンポーネントのソースコードを DL しないと、イベントリスナーの実装や、コンポーネントを再レンダリングした時に、どんな DOM へと書き換えるべきかの情報も得られません。そのため SSR では Hydration がどうしても必要な操作になってきます。
qwik の発明
...のですが、qwik はこれをうまい手法で解決します。qwik はユーザがイベントを発生させるまで、コンポーネントのソースコードを DL しません。逆に、ユーザがイベントを発生させたら、コンポーネントのソースコードを DL して、再レンダリングをします。
ただ、それだけだと DOM にイベントリスナーは設定されていないので、ユーザのイベントをキャッチできません。そこで qwik はランタイム *2 側で一括して window
にイベントリスナーを仕掛けます。
イベントリスナーが呼び出されたら、DOM に書いてある on:click
のような属性を読みに行きます。ここにはコンポーネントのイベントリスナーのコードが書かれているファイル・ファイル内での位置が書かれているので、これを元に、真のイベントリスナーのコードを DL してきて、それを実行します。
つまり、コンポーネントのソースコードの DL 無しにイベントリスナーを登録し、イベントリスナーが発火した瞬間にコンポーネントのソースコードを DL して、再レンダリングをする仕組みになってます。
qwik、「HTML 属性にイベントリスナーの定義場所を書いておき、ランタイムがそれを一括で管理することで hydration 不要でイベントリスナーの登録を実現する」「イベントリスナーが呼び出されたタイミングでテンプレートのコードを DL してくる」というアプローチの組み合わせが発明っぽい
— mizdra (@mizdra) October 23, 2022
コンポーネントのソースコードは、ユーザが何か操作をするまでは DL されません。ページの初期の表示で DL される JS は、qwik のランタイムくらいです。どれだけページが肥大化してバンドルサイズが増えても、初期表示には qwik ランタイム程度の JS しか必要ありません。qwikが「アプリケーションの規模によって初期ロードの JS のサイズが変わらない O(1) フレームワーク」とうたっているのは、これが所以です。
マイクロフロントエンドへの活用
先日 Cloudflare 社から Cloudflare Workers と qwik を組み合わせて、マイクロフロントエンドを実装する PoC の紹介記事が公開されました。
こちらも日本語で解説記事がいくつか出ているので、読んでみると面白いと思います。
マイクロフロントエンドは、巨大なフロントエンドを複数の独立したチームで開発できるようにする開発パターンのことです。とにかく雑に説明すると、チームごとにコンポーネントを作り (動画プレイヤーチームなら動画プレイヤーコンポーネントを、ヘッダーチームならヘッダーコンポーネントを、レコメンドチームなら関連動画一覧コンポーネントを)、チームごとにそれをデプロイして、デプロイしたものを組み合わせて 1 つのページ (動画閲覧ページなど) を作ります。これにより他のチームの手を借りずにチームごとに単独でデプロイしたり、チームごとに好きな技術を選択したり、チームごとにコードの書き直しができたりと、チームごとに独立して開発できる体制を実現できます。
マイクロフロントエンドではチームごとにビルドして、チームごとにその成果物をデプロイします。デプロイがチームごとにされるため、何もしないとコードの共通化が一切されません。例えば複数のチームで、同じバージョンの React のようなライブラリを使っていたとしても、別々に bundle されるので、どちらの成果物にも React のランタイムのコードが bundle されてしまいます。
一応 webpack の Module Federation にこうしたコードを共有する仕組みがありますが、共有するライブラリを手動で選択する必要があり、中々難しいです。
その点 qwik はこの問題を解決するのにうってつけです。というのも、qwik は「プリケーションの規模によって初期ロードの JS のサイズが変わらない O(1) フレームワーク」です。どれだけページが肥大化してバンドルサイズが増えても、初期表示には qwik ランタイム程度の JS しか必要ありません。これはマイクロフロントエンドであっても同じです。ユーザが操作をするまでコンポーネントの JS は DL されないので、どれだけそれぞれのコンポーネントのコードが大きくても、ページの初期表示へのパフォーマンス影響はありません。
qwik 以外の View ライブラリでは、それぞれのチームが初期表示へパフォーマンス影響に注意を払う必要がありましたが、qwik を使うと (一切気にしなくて良いわけでないにせよ) ほとんど気にしなくて良くなります。チーム間で bundle size の上限を巡っておしくらまんじゅうすることなく、よりそれぞれのチームが自律的に動けるようになります。
トレードオフ
夢みたいな技術には多くの場合、それ相応の代償が付いてきます。
イベントリスナー非同期問題
これってイベントリスナが全部非同期になりそうだけど、e.preventDefault() とかどうするんだろう。同期的にリスナが呼び出されないとキャンセルできなさそうだけど。
— mizdra (@mizdra) October 23, 2022
ドキュメントに書いてあった。イベントリスナ側でキャンセルするのではなくて、イベントを呼び出す側がキャンセルを要求するお作法らしい。なるほど〜https://t.co/YQ7V9AzdQi
— mizdra (@mizdra) October 23, 2022
イベントリスナーが呼び出されたら fetch API を叩いて、真のイベントリスナーのソースコードを DL し始めます。そのため、真のイベントリスナーは非同期に呼び出されます。Web 標準のイベントシステムはイベントリスナーが同期的に処理されることを前提に作られているので、非同期にやると色々とほころびが出てきます。qwik 側で独自のキャンセル機構が用意されてますが、標準の方法とは違うのでちょっとびっくりしますね。
一応同期的にイベントリスナーを呼び出す脱出ハッチのようなオプションは用意されているようです。
https://qwik.builder.io/tutorial/events/synchronous/qwik.builder.io
あとはクリックイベントリスナーの実行と CSS のアニメーションのタイミングがユーザからズレて見えたりとか… コンポーネントごとにソースコードを DL する仕組みなので、1回の操作で DL される JS のサイズは小さいはずで、あまり問題にならないような気もする...? 低速な回線を使っているユーザだと違和感を感じられるかもしれない...?
このあたりは Qwik City という Qwik を使ったフレームワークの prefetch で軽減できるようです。ちゃんと用意されているんですねー。
XSS/コンポーネントの DL タイミング問題
on:click に任意のスクリプトファイルの位置情報埋め込むことで XSS の温床にならないかとか、ユーザーインタラクション以外でもコンポーネントの状態は変わりうるけど (例えばsetTimeout(cb, 100) の中で状態を変える)、そういう時はどうやってテンプレートのコードDLするの?とか色々気になりがある。
— mizdra (@mizdra) October 23, 2022
on:click="悪意のあるファイル#悪意のあるイベントリスナーの名前"
みたいな属性を埋め込まれたらどうなるのかとか… 流石に対策してそうではあるけどどうやってるんでしょうね (Issue を探した限りでは言及されてなさそう)。
...まあそういう感じで他にも色々な代償があると思います。
さいごに
@KawamataRyo さんの発表の中であった「アプリケーションの規模によって初期ロードの JS のサイズが変わらない初の O(1) フレームワーク」という言葉に惹かれて調べてみたのですが、面白い発見がいくつもあって良かったです。id:mizdra はマイクロフロントエンドもやらないし *3、React Server Component のようなアプローチのほうが好きなので Not for me かなという感じですが、刺さる人には刺さるライブラリかなと思っています。皆さんも興味があれば触ってみてください。
イベントリスナーの呼び出しが非同期なの、まあまあハマりそうだし、qwik ではなく React Server Component や Island Architecture のほうが個人的には好きかなあ。驚きは小さいほうが良い。(React Server Component や Island Architecture にもまた異なるビックリ要素ある訳だけど…)
— mizdra (@mizdra) October 24, 2022