次世代のUIフレームワークQwikで自作のUUID短縮ライブラリを試せるサイトを作ってみた

に公開

はじめに

株式会社YOSHINANIに外部技術顧問として参加している、株式会社INFLUのNakano as a Serviceです。

先日、私はUUIDをBase58でエンコードすることで短縮するライブラリ 「uuid58」 を開発・公開しました。

https://zenn.dev/yoshinani_dev/articles/001be9fd0377b3
https://github.com/nakanoasaservice/uuid58

これがありがたいことに結構バズったので、このライブラリを手軽に試せるプレイグラウンドサイトを開発することにしました。

こちらが今回作成したプレイグラウンドサイトです↓
https://nakanoasaservice.github.io/uuid58-playground/

ただ単にReactなどのフレームワークで実装するだけでは面白みに欠けると感じたため、フロントエンドエンジニアの mizchi さんが紹介されていた次世代UIフレームワークであるQwikを採用してみることにしました。

本記事では、実際にQwikを使って開発する中で見えてきたQwikの魅力、特にその革新的な遅延ロードの仕組みについて、私の体験を交えながらご紹介します。
なお、この記事は@nakanoaas/uuid58ライブラリ自体の詳細な解説ではなく、それを題材にQwikの技術的な側面に焦点を当てたものであることをご了承ください。

Qwikの魅力:実際に使って分かったこと

実際にプレイグラウンドサイトを開発してみて、Qwikのいくつかの特徴的な利点が明確になりました。

Reactと同じようにJSXで書ける

QwikはReactと同様にJSXを使用してコンポーネントを記述できます。Reactに慣れ親しんだ開発者であれば、学習コストを低く抑えながら開発を始められるでしょう。

以下はプレイグラウンドサイトのメインコンポーネントのコードの一部です。Reactの関数コンポーネントと非常によく似たスタイルで記述できていることがわかります。

import {
  $,
  component$,
  useSignal,
  useStore,
  useVisibleTask$,
} from "@builder.io/qwik";
import type { DocumentHead } from "@builder.io/qwik-city";
import {
  Uuid58DecodeError,
  uuid58DecodeSafe,
  uuid58Encode,
  Uuid58EncodeError,
  uuid58EncodeSafe,
} from "@nakanoaas/uuid58";

// ... (エラーハンドリング用の型定義など)

export default component$(() => {
  const encodedId = useSignal("");
  const decodedId = useSignal("");
  // ... (エラーハンドリングのストア定義)

  const generate = $(() => {
    const decoded = crypto.randomUUID();
    const encoded = uuid58Encode(decoded);
    encodedId.value = encoded;
    decodedId.value = decoded;

    // ... (エラークリア処理)
  });

  // 初回レンダリング後に実行されるuseEffectのようなフック
  // 初回レンダリング後である必然性がなければ使うべきではない
  useVisibleTask$(() => {
    generate();
  });

  return (
    // JSXによるUI記述
    <div class="mx-auto max-w-3xl px-3 py-6 font-sans sm:p-8">
      {/* ... */}
      <input
        type="text"
        value={encodedId.value}
        onInput$={(_, el) => {
          const result = uuid58DecodeSafe(el.value);
          // ... (エラー処理とデコード処理)
        }}
      />
      {/* ... */}
      <input
        type="text"
        value={decodedId.value}
        onInput$={(_, el) => {
          const result = uuid58EncodeSafe(el.value);
          // ... (エラー処理とエンコード処理)
        }}
      />
      {/* ... */}
    </div>
  );
});

余談ですが、開発時に使用しているCursorのAIもQwikの構文を正しく理解し、的確なコード生成や修正を行ってくれました。この点も開発体験の向上に繋がりました。

useStateではなくuseSignal

Qwikでは、状態管理にReactのuseStateに似たuseSignalというフックを使用します。しかし、その挙動には大きな違いがあります。

ReactのuseStateでは、状態が変更されるとコンポーネント全体が再レンダリング(再計算)されます。例えば、以下のようなカウンターコンポーネントの場合、ボタンが押される度にconsole.logの箇所も実行されてしまいます。

// ReactのuseStateの例
import React, { useState } from "react";

function Counter() {
  const [count, setCount] = useState(0);

  // countが変わるたびにここが実行される
  console.log("Counter component re-rendered");

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>Increment</button>
    </div>
  );
}

一方、QwikのuseSignalは、そのSignal(状態)を参照している箇所のみが再計算されます。同じカウンターコンポーネントをQwikで書くと以下のようになります。

// QwikのuseSignalの例
import { component$, useSignal } from "@builder.io/qwik";

export default component$(() => {
  const count = useSignal(0);

  // この部分は初回のみ実行される
  console.log("Counter component evaluated");

  return (
    <div>
      {/* count.value を参照しているこのpタグのみが再計算される */}
      <p>Count: {count.value}</p>
      <button onClick$={() => count.value++}>Increment</button>
    </div>
  );
});

これにより、不要な再レンダリングを防ぎ、パフォーマンスの最適化に貢献します。

この仕組みのおかげで、Reactでパフォーマンスチューニングのためによく用いられるuseCallbackuseMemoといったフックが基本的に不要になります。コードがシンプルになり、ボイラープレートを削減できるのは大きなメリットです。

(ちなみに、Reactもこの課題を解決するためにReact Compilerという新しい仕組みを開発中です。)

すべてが遅延ロードされる

Qwikの最も特徴的な機能の一つが、徹底的な遅延ロードです。Qwikアプリケーションは、ユーザーが操作するまでほとんどのJavaScriptを読み込みません。

例えば、上記のコード例にあるonInput$イベントハンドラは、ユーザーが実際に入力フォームに何かを入力するまではブラウザにロードされません。これにより、初期表示に必要なJavaScriptの量を最小限に抑え、驚くほど高速な初期表示を実現します。

// ...
<input
  type="text"
  value={encodedId.value}
  // このonInput$ハンドラは、ユーザーが入力するまでロードされない
  onInput$={(_, el) => {
    /* ... */
  }}
/>
// ...

Qwikの進化:よりスマートになった遅延ロード戦略

Qwikの代名詞とも言える「すべて遅延ロード」ですが、この戦略も進化を続けています。

遅延ロードの課題とユーザー体験

すべてを遅延ロードするということは、ユーザーが何らかの操作(例えばボタンクリック)をした際に、その操作に必要なJavaScriptをその時点で初めてダウンロードし、実行することを意味します。これにより、インタラクションに対する反応が遅れてしまう可能性がありました。

旧戦略:Service Workerによるバックグラウンドロード

この課題を解決するため、Qwikは当初Service Workerを利用した巧妙な事前ロード戦略を採用していました。

  1. Service Workerの役割: フロントエンドの通信はService Workerを介して行われます。
  2. バックグラウンドでのロード: Service Workerは、ユーザーが何も操作していないアイドルタイムを利用して、まだ読み込まれていないJavaScriptを予測に基づいてバックグラウンドでロードし、キャッシュします。
  3. インタラクション発生時: ユーザーがインタラクションを行うと、必要なスクリプトが要求されます。キャッシュにヒットすれば即座にそれを返し、ヒットしなければそのスクリプトを優先的にロードします。

しかし、この方式には、初回読み込み時にService Worker自体がロード・起動されるまで、ユーザーの最初のインタラクションをブロックしてしまうという課題がありました。Service Workerの起動にはある程度の時間がかかるため、これがボトルネックとなるケースがありました。

新戦略:modulepreload によるメインスレッドでの最適化 (2024/05/21〜)

2024年5月21日、QwikはこのService Workerを用いた事前ロード戦略を廃止し、<link rel="modulepreload"> を活用する新しい方式へと移行しました。

https://qwik.dev/blog/qwik-1-14-preloader/

この新しい仕組みは以下のように動作します。

  1. modulepreloadの活用: スクリプトの事前ロードには、<link rel="modulepreload" href="/path/to/script.js"> のようなHTMLタグが用いられます。
  2. 軽量なローダーによるスケジューリング: アプリケーションの初回ロード時にQwikの非常に軽量なローダースクリプト(qwikloader.js)が実行されます。このローダーが、必要になりそうなJavaScriptモジュールに対応するmodulepreloadタグを、適切な優先順位でDOMに動的に追加していきます。
  3. インタラクションへの動的な対応: 事前ロードが進行中にユーザーがインタラクションを行い、まだ事前ロードされていないスクリプトが要求された場合、Qwikローダーはそのスクリプトの優先順位を動的に引き上げ、他の事前ロードよりも優先してロードします。

このmodulepreloadベースのアプローチは、Service Workerの起動待機によるブロッキングを解消し、よりスムーズなユーザー体験を提供します。また、ブラウザの標準機能を利用することで、より効率的かつ予測可能なリソースローディングが期待できます。

このように、ユーザーのインタラクションの邪魔をせず、かつ効果的にリソースを事前ロードするための優先順位付けを行う仕組みは、私が知る限り他の主要なフロントエンドフレームワーク(例えばReact)には見られない、非常に興味深く、Qwikの先進性を示すものだと感じました。

おわりに

今回のプレイグラウンドサイト開発を通じて、Qwikの持つポテンシャルの高さを垣間見ることができました。特に、その徹底した遅延ロード戦略と、それを支えるための継続的な改善・進化は目を見張るものがあります。

Reactに慣れた開発者にも馴染みやすいJSXベースの記法でありながら、useSignalによるきめ細やかな状態管理や、今回紹介したような先進的な遅延ロードの仕組みは、Webアプリケーションのパフォーマンスと開発者体験の両方を向上させる可能性を秘めていると感じます。

まだまだ発展途上のフレームワークではありますが、今後のQwikの動向から目が離せません。

YOSHINANI

Discussion