Open16

Qwik調べてみたら結構面白かった

aiji42aiji42

Framework reimagined for the edge.
No hydration, auto lazy-loading, edge-optimized, and fun 🎉!

エッジ用に再考されたフレームワーク
Noハイドレート、自動遅延ロード、エッジ最適化

https://qwik.builder.io/

開発元: Builder.io
現在のステータス: Beta

aiji42aiji42

QwikとQwik City

https://qwik.builder.io/qwikcity/overview/

ReactとNext.jsの関係で言うところの、QwikがReactでQwik CityがNext.js

Qwikはコンポネントを構築するためのライブラリ。
jsx,tsxで書けて、その書きっぷりはほとんどReactと同じ。hooksの名前や使い方が若干異なるが、React勢ならドキュメントとサンプルコード呼んだらなんとなくわかるはず。

Qwik Cityは主にファイルベースのNested Routing・Nested Layoutと、Service WorkerによるPrefetchの最適化機能、SSG(*1)、ClowdflarePages・NetlifyEdge・Nodeアダプタを提供

*1: Qwik自体がSSRベース

aiji42aiji42

一旦このスクラップでは、Qwik Cityの説明はPrefetch以外は省略する。

サラッと触れると、ルーティング・レイアウトルールやローダー・ミューテーションモジュールは(onGet/onPost/onPut/onPatch/onDelete)は結構Remixに近い。
(名前が違うだけでほとんどRemixと同じと考えても遜色なさそう)

唯一レイアウトで面白そうなのが、@suffixでディレクトリ構造外のレイアウトを例外的に呼び出せるところ(これがあると地味に便利)

src/
└── routes/
    ├── contact/
    │   └── index@narrow.tsx      # https://example.com/contact (Layout: layout-narrow.tsx)
    ├── layout.tsx                # Default layout
    ├── layout-narrow.tsx         # Default named layout
    └── index.tsx                 # https://example.com/ (Layout: layout.tsx)

あと、ページごとにSSGが選択可能なのはRemixにない機能なので、少し羨ましい。
https://qwik.builder.io/qwikcity/static-site-generation/dynamic-routes/

aiji42aiji42

コンセプト: Redumable (サーバ側の状態をそのままにクライアントで再開可能)

https://qwik.builder.io/docs/concepts/resumable/

Hydrateコストに対しての打ち手。

前提

SSR・SSGにおけるHydrateは、フルレンダリングほどのコストはかからないが、ページが操作可能になるまでにいくつかのステップでコストが掛かっている

  • ページスクリプトのダウンロード
  • スクリプトのパースと実行
  • コンポネントツリーの構築・ステートの復元・イベントリスナの登録

例えばこのページでスクリプト実行開始からHydration完了まで350msくらい
https://www.lifedot.jp/

昨今では、AstroやFreshに代表されるアイランドハイドレーション(部分的なハイドレーション)が、一般的なフルページのハイドレーションに対しての対策という感じになっているが、Qwiqはそれらとは異なる方法で解決している。

ちなみにドキュメントのFAQによれば、アイランドハイドレーションに関して

  • 開発者がアイランドのスコープを自己定義しなければならない
  • スコープを間で通信できない

という欠点をあげており、「それはスケーラビリティがあるとはいえないよね」という見解。

https://qwik.builder.io/docs/faq/#does-qwik-do-partial-hydration

aiji42aiji42

イベントリスナのResume

イベントリスナをシリアル化してDOMに焼き付けている

<button on:click="./chunk.js#handler_symbol">click me</button>

そして、各要素個別でイベントリスナが登録されているのではなく、documetに対してのグローバルリスナが存在する状態になる。
QwikはここでHydrateに相当する処理が完了。
かつ、グローバルイベントリスナのセット処理のスクリプトは初期のHTMLに記述されているので、ここまででスクリプトのダウンロードは行われていない(Zero Loading)。

実際にクリックイベントが発生すると、グローバルのイベントリスナがそれをキャッチし、上記のシリアライズされたチャンクファイルのダウンロードをおこない、実際のイベントリスナの処理を再開する。


Q. ということは、結局操作が行われてから描画に反映されるまでに時間がかかるということ?🤔
A. Qwik単体でアプリケーションを構築するとそうなるが、QwikCityがうまく解決してくれている。

詳しくは後で説明

aiji42aiji42

コンポネントツリーのResume

そもそもコンポネントツリーのHydrateが必要になるのはなぜか?
上記のイベントリスナの件を除くと、再レンダリングを実行するためにコンポネントツリーを構築する必要がある。

QwikではSSR/SSG時にコンポネントの境界情報がシリアライズされてHTMLに書き込まれる。
ドキュメントにどれとは明言されていないが、HTML中に現れるqv始まる謎のコメントがおそらくそれ。

Qwikはコンポネント再レンダリングが必要なるまでコンポネントコードのロードを遅らせる。

再レンダリングが必要になったときに、初めてコンポーネントスクリプトをロードして再レンダリングする。
Reactと異なり、親コンポーネント再レンダリングによって、子コンポーネントが強制的に再レンダリングされるということがない。
コンポーネント自体が階層構造を取るが、それぞれが境界線によって独立しており、Propsで渡されたステートに更新があったときのみ、子コンポーネントが再レンダリングされる。
ステートオブジェクトはProxyで生成されており、props渡す = proxy.subsclibeを実行して状態監視に入るという設計になっている。

aiji42aiji42

コンセプト: Lazy Loading(すべてがデフォルトで遅延ロードになる)

https://qwik.builder.io/docs/concepts/progressive/

いわゆる、Next.jsのDynamic Importのような遅延ロードが自動的に適応される。

初期画面に入らないページ下部のコンポーネントや、モーダルやタブ配下のコンテンツなどのコンポーネントを遅延ロードさせて、初期表示を高速化するという戦略は一般的になりつつある。
ただ、それらは基本的に、選択的な手法であり、どれを遅延ロードしそのタイミングでロードを開始するかという設計と実装が必要になる。

Qwikでは、すべてが自動的に遅延ロード対象であり、そして、いつロードするかということが自動的に最適化される。

  • コンポーネントのレンダリング
  • コンポーネントの状態監視(watch)
  • イベントリスナ
  • スタイル
aiji42aiji42

オプティマイザ

遅延ロードを最適化するためには、コンポーネントの境界線などチャンク可能な分割範囲がビルド時に解釈可能でなければならない

Qwikでは実装者がそれを意識しなくて済むように、「ドルマーク = チャンク可能スコープ」となるように設計されている。

import { component$, useStore } from '@builder.io/qwik';

// チャンク可能領域
export const App = component$(() => {
  const state = useStore({
    count: 0,
  });

  return (
    <>
      <button
        // チャンク可能領域
        onClick$={() => state.count++}
      >
        Increment
      </button>
      <Child  state={state} />
    </>
  );
});

// チャンク可能領域
export const Child = component$(({ state }) => {
  return (
    <>
      <p>{state.count}</p>
      <NotChunk />
    </>
  );
});

//  非チャンク
const NotChunk = (props) => {
  return (
    <div>this is not chunkable</div>
  );
};
aiji42aiji42

Prefetching by Service Worker

https://qwik.builder.io/qwikcity/prefetching/overview/

ここまで紹介してきたQwikが高速化する手法は基本的には、細かい単位でチャンクして遅延ロード。
つまり、Hydrateでやっていたことの実行タイミングを遅らせただけであり、遅延ロードで実通信が起きるなら、ユーザの操作から状態が描画に反映されるまでのレイテンシが増加してしまう。

それを解決するのが、Qwik CityのPrefetching by Service Worker

Service Worker側でマニフェストに従ってバンドルを先読みしてCacheAPIでキャッシュしておき、実際の遅延ロード発生時には、リクエストをインターセプトしてキャッシュストアからスクリプトを返すことで高速化しておくということが行われている

あと、ベースコンセプトが「エッジのために最適化されたFW」をうたっているので、CDN Edgeからチャンクを配信できれば、ブラウザのService Workerなしでもそれなりのパフォーマンスは期待できそう。

https://qwik.builder.io/qwikcity/prefetching/request-response-cache/

Qwikが生成するクライアントバンドルではモジュールがダイナミックインポートで書かれているので、<link rel="modulepreload">でもプリロード自体は可能。
ただし、safari/firefoxでサポートされていないことと、そしてリクエストの重複を避けるためにService WorkerとCacheAPIを採用している。

aiji42aiji42

Qwik CityのWeb上のExampleはなさげ?
とりあえず、yarn create-qwik@latestでローカルで初期構築、yarn previewで実行確認するのが手っ取り早い。

aiji42aiji42

ここまでの戦略とメンタルモデルを支えているのがVite

Viteによるビルドの高速化がこれらの大量チャンクを実現している。

✨ Done in 1.77s.

aiji42aiji42

個人的には、ここまで高速化のためにカリカリにチューニングするかーという関心(からくる面白み)と、その一方でReactライクとはいえ、Reactではないので一旦個人の趣味レベルに留めておきつつ、引き続きウォッチする対象という位置づけ。