この記事は、カケハシ Advent Calendar 2022 の 18 日目 の記事になります。
はじめまして、こんにちは。
おくすり連絡帳「Pocket Musubi」というプロダクトで、エンジニアリングマネージャーをしています @hisasann と申します。
人にフォーカスした開発組織作りに力を入れ、楽しい技術集団を作り上げることに日々奮闘しております。
ぼくは、 20 年近く Web の業界でいて、ソフトウェアエンジニアとして開発をしてきました。
今でも第一線ではないですが、なるべくコードは書いていて、それは好きというのがそもそもですが、日々テクノロジーの変化を楽しんでいます。
特にフロントエンドの分野が好きで、新入社員のころから JavaScript を書いているので、もうかれこれ 20 年近く JavaScript を書いていることになります。
そんなぼくが今回は、エンジニアリングマネージャーとしての話というよりは、もう一つの顔のフロントエンドエンジニアとして、最近気になっている概念とフレームワークをメモしたので、それを記事にさせていただこうかと思います。
はじめに
ここ最近はマイクロフロントエンドという言葉も出てきていて、巨大なフロントエンドを複数のチームに分割して、開発できるようにする方法が考えられていたりします。
これはコンポーネントを分割して開発するというのがイメージしやすく、たとえばヘッダーやフッターはこのチーム、コンテンツ部分はこのチームのように分割していくことで、独立して開発ができるようになるというものです。
言うは易しですが、それがどうやら少しずつ実現できそうな基盤ができているようなのです。
そこで重要になってくるのが Resumable という概念と qwik というフレームワークです。
Resumable vs. Hydration
Resumable の話をする前に、まずは Hydration の流れについて整理しておきましょう。
Hydrationの流れ
SSR(SSG) 時代に、どうやって React などに書かれたイベントハンドラーをフロントエンド側で DOM にアタッチするかの話です。
- サーバーサイドで HTML が何かしらのデータをもとに作成されるとします(SSR)
- これは実際に HTML に埋め込まれる DOM になります
- クライアントサイドに HTML をダウンロードし、 JavaScript のコードたちもダウンロードが終わり実行できる状態になったときに、Component などのコードをもとに HTML を生成し直し、参照透過性のチェックを行う
- 参照透過性
- 同じ props を渡してるのに違うレンダリングがされるのはダメ
- 「サーバーサイドの HTML === クライアントサイドで作った HTML」となるのが期待値
- 参照透過性
- サーバーサイドで生成した HTML にイベントハンドラをアタッチしていきます
- このイベントハンドラの紐付けする作業を Hydration と呼称しています
自分の Zenn に書いた記事ですが、もう少し詳細に書いているので、もしよかったらこちらもご覧ください。
Hydrationの問題点
- コンポーネントの量が増えるとどんどん初期ロードの JavaScript のサイズが増えていく
- JavaScript ファイルのダウンロード・ロード・実行・仮想 DOM の計算とコストが高い
- 静的なページでも同じようにコンポーネントのための JavaScript のコストがかかる
- スマホなどの高速ではない環境だと Time To Interactive が遅い
Resumableとは
既存のフレームワークたちが取っている手法は Replayable(再生型)であり、サーバーサイドレンダリング時の作業を再びフロントエンド側でも実施し、そこまでしてようやくイベントハンドラーをセットしています。
Resumable は、このようにもう一度同じことをするのではなく、フロントエンド側にダウンロードした HTML と小さな JavaScript で一時停止しておき、そこからユーザーのインタラクションによって、 Resume(再開) するという感じになっています。
このように Partial に Hydration を行えるのが、ひじょうに魅力だと思っています。
このあたりは、 Islands Architecture として Astro や fresh といったフレームワークも同じようなアプローチをしていたりします。
qwikを見てみる
ここからいよいよ qwik のことを書いていきます。
イベントハンドラーの遅延ロード+実行
qwik の提供している Link タグを使ったコードの例を見てみましょう。
かなり少ないコードとして端折っていますが、こちらがプロジェクトを作成するとデフォルトでついてくるコードになります。
src/routes/index.tsx
import { component$ } from '@builder.io/qwik'; import { Link } from '@builder.io/qwik-city'; export default component$(() => { return ( <div> <Link class="mindblow" href="/flower"> Blow my mind 🤯 </Link> </div> ); });
上記の JSX 部分を実際にダウンロードした HTML は、ざっくりですがこのようになります。
なんだかいろんなカスタムアトリビュートがついています。
なんとなく click イベントや mouseover などのイベントの雰囲気が感じ取れます。
また特徴的な HTML コメントも見られます。
<!--qv q:id=8 q:key=mYsiJcA4IBc:--> <a href="/flower" preventdefault:click="" class="mindblow" on:click="q-b2ced4c0.js#s_hA9UPaY8sNQ[0 1 2]" on:mouseover="q-b2ced4c0.js#s_skxgNVWVOT8" on:qvisible="q-b2ced4c0.js#s_uVE5iM9H73c" q:id="9"> <!--qv q:s q:sref=8 q:key=-->Blow my mind 🤯<!--/qv--> </a>
そして、 HTML の終了 body タブの直前ぐらいに以下のコードがあります。
<script>window.qwikevents.push("click", "mouseover", "qvisible")</script>
このようにグローバルスコープの window オブジェクトに登録されたスクリプトを使って、 document のルートにイベントを登録しているようです。
このパフォーマンス・チューニングはよく使っていました。
例えば、 100 個のリストが並んでいて、その一行ごとにボタンなどがあった場合、 100 個のボタンにイベントハンドラーをセットすることになるので、その全体を包んでいる div タグなどに 1 個だけイベントハンドラーをアタッチしておき、 event.target を見て目的の要素に対してイベントを fire するという手法です。
そして、このときに登録されるイベントハンドラーはまだダウンロードされていません。
on:click="q-b2ced4c0.js#s_hA9UPaY8sNQ[0 1 2]"
正確に言うと、この q-b2ced4c0.js ファイルは、この要素が 表示された瞬間にロード されます。
なので、 window のサイズが大きければ初期表示時にダウンロードしにいきますが、見えていないようなサイズの場合は遅延ロードされるか、または必要がなければロードしないかです。
こちらのサンプルは Welcome to Qwik になります。
また、 qwik 側も DEMO を用意してくれていて、こちらは時計が見えそうになると JS ファイルがダウンロードされます。
この DEMO は、イベントハンドラー云々よりさらにおもしろく、時計のコンポーネント自体も遅延ロードされているのがわかりやすいです。
JSファイルのチャンク
このように、コンポーネントレベルで分割して、 JS ファイルをチャンクしているのが qwik の特徴ですが、これを実現しているのが、 component$ のように最後に $ がついている関数たちです。
この component$ は、 qwik でチャンクされる分割可能な領域を示しています。
また、 component$ で宣言されたコンポーネントは、常に遅延ロードされるようで、基本的にはつけておけば良さそうですが、 export default
せずに使うコンポーネントなんかはつけないで使用でき、その場合は遅延ロードはされないようです。
https://qwik.builder.io/docs/components/lite-components/
このチャンクですが、ファイル数を分割してビルドするのにはそこそこなコストが掛かります。
それを爆速で実現しているのが、 Vite です。
どれぐらい速いかは、以下の qwik のスターターを見に行くと面白いです。
Qwik Starter (forked) - StackBlitz
遅延ロードすると体感が悪くなるのではないか
このあたりも qwik 側はちゃんと考えているようで、リクエストしたファイルを Cache API で先読みして事前にキャッシュしておき、実際にインタラクションのときにリクエストが発生したら、 Service Worker を使ったネットワークインターセプトをすることで、高速化しているようです。
https://qwik.builder.io/docs/advanced/prefetching/#frequently-asked-prefetching-questions
なので、どのくらい事前にキャッシュ用にリクエストしているかはマニフェストファイル次第ですが、完全に JS ファイルをリクエストしていない、というわけではなさそうです。
それでも、プリフェッチしたファイルは実行はしないので、パフォーマンスにはかなり低コストな影響となるでしょう。
マイクロフロントエンドとしてのqwikの利用
チームごとに作られたコンポーネントは、たとえばそれぞれのコンポーネントが別々のバージョンの同じライブラリを使っていることなんてことがあると思います。
これは一昔前にあった、 jQuery がいろんなバージョンが使われていて、バッティングしないように $ は使わず $j や j$ のような名前空間に jQuery を格納することをしていました。
var $j = jQuery.noConflict(true);
これでは、すべての jQuery のバージョンが初期ロード時にダウンロードされてしまいますが、 qwik の遅延ロードを使うことで、実際に使うまではそのライブラリはダウンロードされないので、ダウンロードの負荷による遅延の影響度を最小限にすることができます。
こういったところがパーツを複数のチームにわけても、そこまで問題にならないのではないか、というのがマイクロフロントエンドに大きく貢献できそうです。
さいごに
qwik を素振りして、少し調べてみたことをメモしてみました。
Resumable という概念がひじょうに面白く、時代とともに生まれた技術的な困難をなんとか解決できないかと賢い人達が試行錯誤しているのが伺えました。
ここ1年ぐらい、 Notion で日記を書いてきているのですが、 Next.js で SSG したものを Vercel にアップしています。
これを、 Cloudflare Pages + qwik で SSR できたら面白いかなと思って今コードを書いている最中です。
冬休みの宿題にでもしようかと思っています。
それでは皆様、風邪など引かぬように暖かくして年末をお楽しみください。
読んでいただいてありがとうございました。