🧊

Qwik(City)を試してみた感想

もとい、Svelte(Kit)なプロジェクトを、Qwik(City)で書き直してみての学び。

待望のやつではあるが、端切れの悪いタイトルなのは、移行途中でどうしようもなくハマってしまい、採用を見送ることにしたから・・😇

そういうわけなので、使い込んでみたわけでもまったくないし、最高!とか微妙・・・とかそういう判断というよか、ただの感想文って感じ。

QwikCityありき

`npm create qwik`のスターターが、そもそもQwik単体ではなく、QwikCityがセットになってた。

今回はSSG/SPA用途だったので、できればQwikだけ使えればいいかなーと思ってたけどそんな選択肢はなかった。こういうメタフレームワークSSRが前提になってたりすることが多くて、SSRなコンテキストを回避するコードを書きたくないし、挙動としても囚われたくなかったのに・・。

あとはコードベースもTypeScript一択になってて、拡張子を適当に変えてみるとビルドできなくなったりと、当面このへんはどうにもならなさそう。
スターターが生成するファイル群(`entry.xxx`とか`root.tsx`とか)も、場所を動かすことすら許されない感じだった・・・。

SSRから逃げられない

上述のとおり、完全にSPAとして作りたい場合(将来的なSSRへの移行なんかも考えなくて良い)は、そもそもコードのすべてがクライアントで実行されてしまっていい。SSG的な初回SSRもいらない。

という場合に、コードベースからSSR成分を完全に排除できるメタフレームは、知る限りSvelteKitしかない。(根本で`export const ssr = false`すればいい)

この設定ができないNextとかSolidStartとかだと、それ用の`NoSsr`みたいなコンポーネントを自作する必要がある。(`useEffect()`でフラグを立てるまではメインの`children`を描画しないみたいなやつ)

が、Qwik(City)では、この作戦も通用しなかった。

const NoSsr = component$(() => {
  const csr = useSignal(false);
  useClientEffect$(() => { csr.value = true; });

  return csr.value ? <Slot /> : <div>LOADING...</div>;
});

理論上(コンポーネントツリー的な)は問題ないと思うし、コンパイルも通るけど、実際には下層のコンポーネントまでしっかりSSRなコンテキストで事前実行されてしまうらしく、普通に`localStorage is not defined`みたいになる。

下層のコンポーネントであっても、Qwik-wayに遅延ロードできるチャンクに変換すべく、一通りは実行する的な挙動っぽい。そういう意味では、常にすべてのコンポーネントをユニバーサルなコードとして紡ぐ必要があり、そうなると必然的にブラウザでだけ動くAPIを使ったコードをよく使うプロジェクトは書きにくい・・よな〜って。

https://qwik.builder.io/docs/components/rendering/

Out-of-orderっていう概念がたぶんそれ。

Think Qwikむずい

Qwikを支える、Resumability & Serializationを理解し、コードに落とし込めるようになるまで、めっちゃ時間かかりそうって話。

まあ既存のものとは一線を画するFWっていう意味では、学習コストが高いのは仕方ないというか、そういうものなんやけど。

たとえば、遅延実行のために`$`で隔てられた境界を超えるためには、HTMLの属性値としてシリアライズできる必要がある。そうなるともちろんJSONにできないもの(`class`とか関数とか)を、特別な方法で扱う必要が出てくるわけで。そのために`noSerialize()`っていうAPIでラップしたりとひと手間が必要で、ここ自動じゃないんや・・ってなる。

あとは内部的にQRLと呼ばれてるQwikの最小実行単位みたいな枠に収まるコードにする必要もあって、たとえばこういうのがエラーになる。

const WORLD = "world";

export default component$(() => {
  return (
    <p>Hello, {WORLD}</p>
  );
});

こういう雑なスコープの変数は、`$`境界を超えて扱うことができない。

明示的にファイルを切って`export`するとか、インラインで書く必要がある。

import { WORLD } from "..."

export default component$(() => {
  return (
    <p>Hello, {WORLD}</p>
  );
});

// OR

export default component$(() => {
  const WORLD = "world";

  return (
    <p>Hello, {WORLD}</p>
  );
});

インラインだとしても、関数なんかを変数化するにはひと手間が必要。

export default component$(() => {
  // const onClick = () => console.log("world!");
  // Need to wrap with `$`
  const onClick = $(() => console.log("world!"));

  return (
    <button onClick$={onClick}>Hello</button>
  );
});

どこまでもコンパイラの気持ちにならないといけない。

まあコンパイラがいちいち賢く怒ってくれるので、ある程度はなんとかなるとはいえ、めんどくさいなって。

他にも、イベントハンドラまでも非同期なので、`ev.preventDefault()`も特別な書き方が必要とかある。

export default component$(() => {
  return (
    <form onSubmit$={() => { /* ... */ }} preventdefault:submit>
      ...
    </form>
  );
});

React知ってればQwik書けるぜ的な宣伝のされ方してるけど、ぜんぜんそんなことない。

あとはコンパイラが怒ってくれないけど落ちるみたいなやつを踏んだ時、本当にどうしようもない・・・。(それで採用やめた勢としては)

自動で遅延ロードされる難しさ

QwikCityでは、`ServiceWorker`がprefetchをめちゃめちゃがんばってる。(Qwik単体だとプリフェッチの仕組みはなかったはず)

Qwik City Caching Request and Response Pairs - Qwik

`link[rel=prefetch]`よりも洗練されてるらしい。複数の場所から同じリソースを読もうとしたときに、リクエストが重複しちゃうとかそういう心配がないとのこと。

ただ、帯域やパケットという意味ではどうなんだという気持ちになっちゃう。確かに実行は遅延されるけど、せっかくダウンロードしたのに実行されないコードも出てくるので。

このへんは全自動だとやっぱりむずかしいのかなーと。小さいフォームがあるだけのページで、フォーム要素それぞれの`input.onInput`を遅延読み込みする必要あるか?とか考えてしまう。

あとはチャンクが細かくなるということは、Svelteのそれみたく、ある一定以上の規模になると、ボイラープレートがかさんでトータルのサイズが大きくなってしまう問題もありそう。サイズは目的としてないってどこかに書いてあったような気もする。

そういうデザインなので仕方がない。

結局いつ輝くのか

個人的にずっと思ってるやつ。

すべてが遅延ロードできる = ロードしたいものがたくさんある前提、というところで、JavaScriptの総量が少ないサイトではそもそも採用意義が薄いはず。

なぜかSPA化されてしまってる静的な成分が多めのサイトであれば、まずはMPAにするとか、Astroくらいの遅延実行でも十分というか。部分だけSSRできるようにもなったしなおさら。(もちろんハイドレーション自体が・・というのはあるけど)

となるとやはり対象は、巨大なSPAなのかなと思うけど、クライアント専用のJSが多いコードは、上述の理由からあまり書きやすいとは思えず。もちろんパフォーマンスのために開発者が骨を折るのは理解できるとしても。

でもそれも、ルートごとにやるのはもちろん、特別に重いとか優先度の低いコンポーネントを、手動で`lazy()`に読み込むとかでそれなりにカバーできそうな気もしていて。

というところで、相変わらず自分のこれまでの経験と、それらに対する択という意味ではあまり刺さるシーンがない気がするなあというこの気持ちは、実際にコードを書いてみてもあまり変わらんかった。

おわりに

すべてを遅延ロード・遅延実行できるのは確かに理想的ではあるが、そのためにかけられるオーサリングのコストとしてはやや予算オーバーで、遅延の単位ももう少し調整させてほしいな〜というのが正直なところ。

そもそも現時点ではまだベータなので、そりゃあ思ったほどすんなりはいかんよってことは承知の上で、これからに期待しつつ、またの機会に試そうかと。

せっかく書きやすいReactiveプリミティブ(`useSignal()`とか`useResource$()`とか)や`useStylesScoped$()`あるので、SPAがもっと楽に書けるようになってほしいな〜。