qwikをざっくり調べてみた
( レポより )
Qwikとは?
一言で言うと、 SSRのパフォーマンスを極限まで高めるFrontend Frameworkです。
その仕組みを一言でいうと、 コードに非同期な境界を定義してとことん分割することで、必要なコードを必要な時に順次非同期にDL・実行できる。
(これによって、Hydration処理を遅延させ、パフォーマンス劣化を防ぐことができる。)
Qwikのステータス・特徴
- 23年5月に1.0がリリースされたばかり
- シンタックスは基本Reactに合わせている
- VDOMを利用しない (DOMに外科的変更を加える)
- Star History
- 競合と比べるとStarが少ない
builders.io
- Webページ制作のNoCodeツールを展開している会社
- 他のOSS
- partytown
- 3rd Parthのコードをworkerで実行する
- https://partytown.builder.io/
- mitosis
- コンポーネントコードのユニバーサル化
- https://github.com/builderio/mitosis#quick-start-guide\
- (尖ったOSSが多そう)
- partytown
Hydrationの問題点
Hydrationとは?を軽く補足します。
SPAアプリケーションをSSRする際、サーバサイドでコードを実行することでHTMLや状態が出力(レンダリング)されますが、ブラウザに送信する際には静的なファイルに変換しなくてはならないので多くの情報が失われます。
例えば、
- オンメモリの情報
- 実行済のJS
- VDOM
- DOMのEvent Listener
そのため、ブラウザ上では一見高速にページが表示されているように見えますが、実はSSRされたHTMLはほぼハリボテ状態で、アプリケーションとしてインタラクティブに動作させるには、SPAフレームワークをフルで再実行(ある程度状態が記録されているにしろ)する必要があります。
Hydrationは「水戻し」という意味ですが、SSRによって生成されたSPAとしてはカラカラなHTMLをフロントエンドでふやかしていく作業を指し、この部分がWebパフォーマンスのネックになります。
Qwikが開発されたモチベーション
- Hydrationによるパフォーマンス劣化を防ぎたい
- コードをとことん非同期に分割して、必要なコードだけ順次実行できる様にする →
Resumability
(再回可能)
- コードをとことん非同期に分割して、必要なコードだけ順次実行できる様にする →
- LazyLoadの境界面をDeveloperが設計するのは限界がある
- 低レベルから最適化する必要がある
QwikのSolution
以下の3点に関して解決策を示しています。
イベントリスナー
通常のSSRだと、ブラウザ側でDOMの必要な箇所すべてにイベントリスナを再設置する必要がありますが、Qwikの場合は、単一のGlobalListener
のみ設置します。
では、どのようにインタラクティブに動作するのか?ですが、
まず、Qwikはコードをとことん非同期に分割すると言うことを前述しましたが、イベントハンドラ
単位でもコードが分割されます。
また、SSR時にHTML上にイベント処理に対応するコードの位置が直接埋め込まれます。
ex)
<button on:click="./chunk-a.js#Counter_button_onClick[0]">0</button>
on:click
の部分にQRL
という独自形式のパスが指定されており、このボタンのクリックイベントを処理するハンドラのコードの場所を指定しています。
最初に述べた単一のGlobalListenerにQRLのLoaderが含まれているため、ボタンをクリックをしたタイミングで初めて、QRLに記載されたコードがDL・実行されます。
また、ネットワークのレイテンシを考慮すると、結局ボタンを押した時の動作が緩慢になってしまう様に思われますが、ServiceWorkerを利用して自動的にPrefetchしておくことで、気にならないくらいの速度を実現している様です。
component tree
通常のSSRでは、描画されたHTML上にコンポーネントの境界情報が失われているので、SPAフレームワークを再実行して初めてDOMとVDOM(状態)の対応が判明します。Qwikの場合は、HTML上にコンポーネントの境界を明記します。
また、従来のSPAではコンポーネントツリーの中で、親コンポーネントが子コンポーネントを描画するので、子コンポーネントを単体で描画することはできなかったのですが、 Qwikの場合はコンポーネント単位でもコードが非同期に分割されており、どのコンポーネントも単体で描画できます。
application state
QwikではState(Store)の実態がProxyになっています。 Proxyを補足するとgetter/setterを定義したClassのような物で、プロパティへのアクセスをすべて監視することができます。
これにより、コンパイル時にあるStateが変更された時に影響を受けるコンポーネントの一覧がSubscriptionListとしてHTMLにシリアライズされます。
ブラウザ側では、あるStateが変更された時は、SubscriptionListから依存するコンポーネントが判明します。 Qwikはこの時点で初めて対象のコンポーネントの描画関数(QRLに分割されている)をDL・実行します。
Qwikのコード分割の様子
(公式より)
before
export const Counter = component$(() => {
const count = useSignal(0);
return <button onClick$={() => count.value++}>{count.value}</button>
})
- コンポーネント定義が
components$
で囲われる useState
=>useSignal
(useStore)onClick
=>onClick$
Qwikでは暗黙な非同期境界ができない様に、非同期部分を $
で明示します。
例えば、 コンポーネントの記述が component$()
で囲われていますが、コンポーネントコードが一つの非同期分割単位(非同期境界)になっていることを示しています。
after
const Counter = component(qrl('./chunk-a.js', 'Counter_onMount'));
chunk-a.js
export const Counter_onMount = () => {
const count = useSignal(0);
return (
<button onClick$={qrl('./chunk-b.js', 'Counter_onClick', [count])}>
{count.value}
</button>
)
}
const Counter_onClick = () => {
const [count, props] = useLexicalScope();
return (count.value += props.step || 1);
};
React,Nextとの対応
- component
export const = () => {}
=>export const = component$(() => {})
- Lifecycle (
useEffect
)useTask$
,useVisibleTask$
- State (
useState
)useSignal
,useStore
,useResource$
- event
onClick={() => {}}
=>onClick$={() => {}}
getServerSideProps
=>routeLoader$
Routing
qwik
がコアでqwik-city
がフレームワーク- ReactにとってのNextのようなもの
qwik-city
にNextのPageRouterと同様のディレクトリベースルーティングが実装されている- (最近はAppRouterもあるけど、どうなるんだろう🤔)
その他の特徴
- Viteでビルドする
- Integration が多数あ
る
- playwrightやStorybookがコマンド一発で追加できるのは嬉しい😊
.md
,.mdx
もデフォルトでページとして認識される- Reactコンポーネントを利用することができる
- ReactコンポーネントだけHydrationされる
- マルチコンテナ可能
- (サンプルは見つからず、、)
- 1ページに複数のQwikアプリケーションを配置できる
- 実例) Cloudflare Workers and micro-frontends: made for one another
考慮点
- 非同期に分割されることの影響を理解してコーディングする必要がある
- 非同期で受け渡す値にはシリアライズ可能な形式が要求される
- ex) Component Props
- 非同期で受け渡す値にはシリアライズ可能な形式が要求される
感想
ある意味でSSRの究極系とも言える技術だと思いました。Svelteなどコンパイラを利用した最適化よりもう一段踏み込んでいる部分が非常に興味深く感じます。 シンタックスなどはReact風とのことですが、開発に当たってはQwikのコンセプトを深く理解する必要があるので、開発難易度はかなり高めではないかと想像しています。
現職では残念ながら実践投入する場所がなさそうなので、いずれ個人のプロジェクトで活用してみたいと思います。
末筆ながら、ここまで読んでいただいてありがとうございます。