Skip to content

Instantly share code, notes, and snippets.

@mizchi
Created January 17, 2024 11:39
Show Gist options
  • Save mizchi/f3ec2e7e940a888fedd746b58cc43236 to your computer and use it in GitHub Desktop.
Save mizchi/f3ec2e7e940a888fedd746b58cc43236 to your computer and use it in GitHub Desktop.
theme marp
gaia
true

Qwik それはフロントエンドの見た夢 Long Version

@mizchi | PWA Night


About

  • mizchi | Plaid, Inc
  • Node.js, Frontend エンジニア
  • この資料は Workers Tech Talk の LT(5min) を膨らませたもの
  • 最近は League of Legends を10年ぶりにやってる

Qwik とは...


見た目は React!

中身はコンパイラ!


Qwik とは

  • https://qwik.builder.io/
    • BuilderIO社 (ノーコードプラットフォーム)が開発
    • パフォーマンスに全振りした過激な設計が特徴
  • Pros
    • React JSX 方言でノウハウを転用できる
    • 採用するだけでパフォーマンス問題は(ほぼ)解決
    • 使いこなせれば理論上最強
  • Cons
    • 最適化のためにかなり特殊な制約がかかる

Qwik 始め方: npm create vite

$ npm create vite@latest
✔ Project name: … qwik-app
✔ Select a framework: › Qwik
? Select a variant: ›
    TypeScript
    JavaScript
❯   QwikCity ↗
  • 運用上 SSR 必須なので最初から QwikCity がおすすめ
    • 一応 Static Export もある

Qwik: 実際のコード例

import { component$ } from "@builder.io/qwik";
export default component$(() => {
  console.log("render");
  return <button onClick$={() => console.log("hello")}>
    Hello Qwik
  </button>;
});
  • 見た目は React JSX だが...
  • 素朴なマークアップなら React とほとんど同じコードになる
  • ...$ で終わると、チャンク分割の合図 (詳細は後述)

Qwik の特殊な制約

  • Qwik Optimizer を前提としたコーディング(後述)
  • 運用上クラサバが緊密に連携する為 QwikCity(Qwik版Next) か Astro が前提

Qwik: Resumable

  • Qwik の主要なコンセプト
  • SSR 前提で、必要なチャンクを都度渡す
    • <script type="qwik/json">... に状態をシリアライズ
    • イベントハンドラごとに チャンクを取得して実行
  • Hydration(React etc...) との違い
    • クラサバの冪等性の為に同一テンプレートを再展開のが Hydration
    • Resumable はSSR前提でクライアントの差分だけ渡す

Qwik Optimizer が強すぎる


Qwik コンポーネントの例

import { component$ } from "@builder.io/qwik";
export default component$(() => {
  console.log("render");
  return <button onClick$={() => console.log("hello")}>Hello Qwik</button>;
});
  • ...$() で常にチャンクが分割される
    • component$(...) で別チャンク
    • onClick$(...) で別チャンク

生成されるコード: 1

// app.js
import { componentQrl, qrl } from "@builder.io/qwik";
const App = /*#__PURE__*/ componentQrl(
  qrl(
    () => import("./app_component_akbu84a8zes.js"),
    "App_component_AkbU84a8zes"
  )
);
  • component$(...) の内容が別チャンク

生成されるコード: 2

// app_component_akbu84a8zes.js
import { jsx as _jsx } from "@builder.io/qwik/jsx-runtime";
import { qrl } from "@builder.io/qwik";
export const App_component_AkbU84a8zes = () => {
  console.log("render");
  return /*#__PURE__*/ _jsx("p", {
    onClick$: qrl(
      () => import("./app_component_p_onclick_01pegc10cpw"),
      "App_component_p_onClick_01pEgC10cpw"
    ),
    children: "Hello Qwik",
  });
};
  • onClick$(...) が別チャンクに

生成されるコード: 3

// app_component_p_onclick_01pegc10cpw.js
export const App_component_p_onClick_01pEgC10cpw =
  () => console.log("hello");
  • onClick で実際に呼ばれるコード

QRL: $(...)

  • Qwik 内 URL 参照オブジェクト
  • $(...) で自分で生成することもできる
import {$, component$} from "@builder.io/qwik";
const C = component$(() => {
  const V = 1; // Serialize できるので他から参照できる
  const f1 = $(() => console.log(V)); // QRL化された関数
  const f2 = $(() => f1()); // QRL 同士なので参照できる
  // handler として QRL 化された関数を受け取ることができる
  return <button onClick$={f2}>btn</button>
})

Qwik で動かないコード: 1

const C = component$(() => {
  const v1 = new (class {})();
  let v2 = 1;
  const f1 = $(() => {
    // NG: class はシリアライズできない
    console.log(v1);
    // NG: 別チャンクの let を書き換えることができない。signal が必要
    v2++;
  });
});

Qwik で動かないコード: 2

// NG: QRL化されてない関数ハンドラは受け取れない
const C = component$(() => {
  const f1 = () => { console.log(1) };
  return <div onClick$={f1}>btn</button>
});

// OK: 関数リテラルなら静的解析でチャンク化される
const C = component$(() => {
  return <div onClick$={() => { console.log(1) }}>btn</button>
});

スコープ解析と Signal/QRL

  • スコープ解析により signal と QRL は子チャンクに引き継がれる
const C = hcomponent$(() => {
  // --- onMount chunk ---
  const singal = useSignal(0);
  const handler = $(() => {
    // ---- handler chunk ----
    singal.value++
  });
  // --- onRender chunk ---
  return <div onClick$={handler}>{signal.value}</button>
});

onMount 最適化で察する Qwik Optimizer

const Greeter_onMount = (props) => {
  const salutation = "Hello";
  return qrl(
    "./chunk-b.js",
    "Greeter_onRender",
    // スコープ解析によって参照可能なものを渡す
    [salutation, props]
  );
};
  • チャンク分割でスコープがぶつ切りになるのでシリアライズ/QRL化が必須
  • let が引き継げないのはコンテキストが消失するため

Qwik Loader

<body q:base="/build/">
  <button on:click="./myHandler.js#clickHandler">push me</button>
</body>

QwikCity

  • Qwik 版 Next.js (Vite SSR Plugin)
  • Adapter: node, deno(-deploy), cloudflare, vercel, cloudrun
// src/routes/with-loader/index.tsx => /with-loader
import { component$ } from "@builder.io/qwik";
import { routeLoader$ } from "@builder.io/qwik-city";
export const useServerData = routeLoader$(async (requestEvent) => {
  return { serverTime: Date.now() };
});
export default component$(() => {
  const signal = useServerData();
  return <p>ServerTime: {signal.value.serverTime}</p>;
});

Qwik 書き味


qwik-react

/** @jsxImportSource react */
import { qwikify$ } from '@builder.io/qwik-react';
function Greetings() { return <div>Hello from React</div>; }
export const QGreetings = qwikify$(Greetings, {
  eagerness: 'hover' // Hydration するタイミングを指定
});
  • https://qwik.builder.io/docs/integrations/react
  • Qwik 内で本物の React を SSR + Hydration できる
  • Astro のアイランドアーキテクチャと同じ、 Hydration するトリガーをコントロールできる
  • ↑ の例だとマウスを hover したとき

Qwik エコシステムへの不満

  • UI ライブラリが足りてない
    • Qwik UI ぐらい?
    • ほぼフルスクラッチで書く気概が必要
    • 複雑な箇所は qwik-react を使ったほうがよさそう
  • 自作した

QwikCity

  • Qwik 版 Next
    • 現状 Qwik をホストする為にほぼ必須(or Astro)
  • 流行りの File Based Routing
    • SSR/CSR
    • DataLoader/ServerAction
  • プラットフォーム毎のアダプタ
    • node, deno, cloudflare, cloudrun, firebase, StaticSite...

QwikCity: DataLoader

import { component$ } from '@builder.io/qwik';
import { routeLoader$ } from '@builder.io/qwik-city';
export const useMyData = routeLoader$(async (requestEvent) => {
  return {...}
});
export default component$(() => {
  const signal = useMyData();
  return <pre>{JSON.stringify(signal.value, null, 2)}</pre>;
});
  • ルーティング時のデータ取得
  • async component / getServerSideProps 相当

QwikCity: Cache Control

import type { RequestHandler } from "@builder.io/qwik-city";
export const onGet: RequestHandler = async ({ cacheControl }) => {
  cacheControl({
    public: true,
    maxAge: 5,
    sMaxAge: 10,
    staleWhileRevalidate: 60 * 60 * 24 * 365,
  });
};
  • リクエストヘッダを付与
  • (ヘッダを尊重するかはデプロイするプラットフォーム次第)

QwikCity: auth.js

import { component$ } from '@builder.io/qwik';
import { useAuthSignin } from '~/routes/plugin@auth';
export default component$(() => {
  const signIn = useAuthSignin();
  return (<button onClick$={() => signIn.submit(...)}>Sign In</button>);
});

QwikCity 要約

  • だいたい Next.js/Remix と同じ機能はある
  • サンプルが少ないのでドキュメント読んで実装する基礎力が必要

Astro: Qwik Integration

https://docs.astro.build/ja/install/manual/

// astro.config.mjs
import { defineConfig } from 'astro/config';
import qwikdev from '@qwikdev/astro';
export default defineConfig({ integrations: [qwikdev()] });
  • 現状 Qwik でウェブサイトを作るなら一番手頃な選択肢
  • Jamstack 的な低頻度の更新なら QwikCity より Astro のが運用が楽
    • 例えばブログやドキュメントサイト

Qwik: 実用上気をつけること

  • 小さなアセットを高速に配信する必要がある
    • CDN ではないサーバーから配信するとRTTで悪化する
    • おそらく Cloudflare Pages が最適
    • 一応 ServiceWorker による先読みも入っているが...
  • Qwik Optimizer を意識したコーディングが必須

おわり

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment