シングルページアプリケーション以外ではインタラクティブなWebアプリは構築できない...そしてその他の神話

Tony Alaribe

#ブラウザの進歩への賛歌。

私はRedditやYCombinatorで、新しい開発者が技術スタックのアドバイスを求める議論によく遭遇します。必然的に、ReactやAngularJSのようなシングルページアプリケーション(SPA)フレームワークを使わなければ、高品質なアプリケーションを構築することは不可能だという主張が出てきます。これは奇妙に感じます。なぜなら、SPA革命以前でも、多くの一般的なマルチページWebアプリケーションが優れたユーザーエクスペリエンスを提供していたからです。

2年前、私はオブザーバビリティプラットフォームを構築することを決意し、HTMXを使用したマルチページアプリケーション(MPA)アプローチを試してみることにしました。ほとんどのオブザーバビリティプラットフォームはReactJS上に構築されていることを考えると、サーバーレンダリングされたMPAはデータ量の多いアプリケーションには不適切なのでしょうか?

私が発見したのは、特定の点に注意を払えば、優れたサーバーレンダリングアプリケーションを作成できるということです。

よくあるMPAの神話と、私がそれらについて学んだことをご紹介します。

#神話1: MPAのページ遷移は、すべてのページ遷移でJavaScriptとCSSがダウンロードされるため遅い

MPAのページ遷移が遅いという認識は広く普及しています。そして、これはブラウザのデフォルトの動作であるため、全く根拠がないわけではありません。しかし、ブラウザはこの問題を軽減するために、過去10年間で大幅な改善を遂げてきました。

例として、以下のビデオでは、キャッシュを無効にした状態での完全なページリロードは、DOMContentLoadedイベントが発生するまで2.90秒かかります。これはWi-Fiの電波が悪いカフェで記録したものですが、これを基準点として使用しましょう。この数字を覚えておいてください。

MPAでは、**PJAX、Turbolinks、さらにはHTMX Boost**などのライブラリを使用して読み込み時間を短縮するのが一般的です。これらのライブラリは、Javascriptを使用してページのリロードをハイジャックし、遷移間でHTMLのbody要素のみをスワップアウトします。そのため、ページのheadセクションにあるアセットのほとんどは、再読み込みや再ダウンロードする必要がありません。

しかし、ページ遷移中に再ダウンロードまたは評価されるアセットの量を減らす、あまり知られていない方法があります。

#サービスワーカーによるクライアントサイドキャッシング

SPAフレームワークでプログレッシブWebアプリケーション(PWA)を構築したフロントエンド開発者は、サービスワーカーについて知っているかもしれません。

フロントエンド開発者やPWA開発者でない方のために説明すると、サービスワーカーはブラウザに組み込まれた機能です。ユーザーとネットワークの間に位置するJavascriptコードを記述し、リクエストをインターセプトして、ブラウザがそれらをどのように処理するかを決定することができます。

service-worker-chart.png

PWAトレンドとの関連性から、サービスワーカーはSPA開発者の中では当たり前のものですが、この技術は通常のマルチページアプリケーションにも使用できることを開発者は認識する必要があります。

ビデオデモでは、サービスワーカーを有効にして現在のページをキャッシュし、更新します。ページをリロードするためのリンクをクリックしたときにちらつきがないことに気づかれるでしょう。その結果、よりスムーズなユーザーエクスペリエンスが実現します。

さらに、以前のように2MB以上の静的アセットを送信する代わりに、ブラウザは अब केवल 84KB のHTMLコンテンツ(実際のページデータ)のみをフェッチします。この最適化により、`DOMContentLoaded`イベントの時間は2.9秒から500ミリ秒未満に短縮されます。驚くべきことに、この改善はHTMX Boost、PJAX、またはTurbolinksを**使用せずに**実現されています。

#マルチページアプリケーションにサービスワーカーを実装する方法

自分のMPAでこれらのパフォーマンス向上をどのように再現するのか疑問に思っているかもしれません。簡単なガイドをご紹介します。

  1. **`sw.js`ファイルを作成する**: これは、キャッシングとネットワークリクエストを管理するサービスワーカースクリプトです。
  2. **キャッシュするファイルをリストアップする**: サービスワーカー内で、キャッシュするすべてのアセット(HTML、CSS、JavaScript、画像)を指定します。
  3. **キャッシング戦略を定義する**: 各タイプのアセットをどのようにキャッシュするかを指定します。たとえば、永続的にキャッシュするか、定期的に更新するかなどです。

サービスワーカーを実装することで、ブラウザにネットワークリクエストとキャッシングの処理方法を効果的に指示し、読み込み時間を短縮し、マルチページアプリケーションの全体的なパフォーマンスを向上させることができます。

#Workboxを使用してサービスワーカーを生成する

サービスワーカーを手動で記述することも可能ですが—そしてこのMDNの記事のような優れたリソースが役立ちます—私はGoogleのWorkboxライブラリを使用してプロセスを自動化することを好みます。

#Workboxを使用する手順

  1. **Workboxをインストールする**: npmまたは好みのパッケージマネージャーを使用してWorkboxをインストールします。

    npm install workbox-cli --global
    
  2. Workbox設定ファイルを生成する: 次のコマンドを実行して設定ファイルを作成します。

    workbox wizard
    
  3. **アセットの処理を設定する**: 生成された`workbox-config.js`ファイルで、異なるアセットをどのようにキャッシュするかを定義します。`urlPattern`プロパティ(正規表現)を使用して、特定のHTTPリクエストに一致させます。一致する各リクエストに対して、`CacheFirst`や`NetworkFirst`などのキャッシング戦略を指定します。

    workbox-cfg.png

  4. **サービスワーカーをビルドする**: Workboxビルドコマンドを実行して、設定に基づいて`sw.js`ファイルを生成します。

    workbox generateSW workbox-config.js
    
  5. **アプリケーションにサービスワーカーを登録する**: HTMLページに次のスクリプトを追加して、サービスワーカーを登録します。

    <script>
      if ('serviceWorker' in navigator) {
        window.addEventListener('load', function() {
          navigator.serviceWorker.register('/sw.js').then(function(registration) {
            console.log('ServiceWorker registration successful with scope: ', registration.scope);
          }, function(err) {
            console.log('ServiceWorker registration failed: ', err);
          });
        });
      }
    </script>
    

これらの手順に従うことで、ブラウザに可能な限りキャッシュされたアセットを提供するように指示し、読み込み時間を大幅に短縮し、マルチページアプリケーションの全体的なパフォーマンスを向上させることができます。

Image showing the registered service worker from the chrome browser console.

Chromeブラウザのコンソールから登録されたサービスワーカーを示す画像。

#`Speculation Rules API`: インスタントページナビゲーションのためのページのプリレンダリング。

**htmx-preload**または**instantpage.js**を使用したことがある場合、プリレンダリングと「Speculation Rules API」が解決しようとしている問題に精通しているでしょう。Speculation Rules APIは、将来のナビゲーションのパフォーマンスを向上させるように設計されています。現在のページでどのリンクをプリフェッチまたはプリレンダリングするかを指定するための表現的な構文があります。

Speculation rules configuration example

Speculation Rulesの設定例

上記のスクリプトは、Speculation Rulesを設定する方法の例です。これはJavascriptオブジェクトであり、詳細には触れませんが、「where」、「and」、「not」などのキーワードを使用して、どの要素をプリフェッチまたはプリレンダリングするかを記述できることがわかります。

プリレンダリングの影響の例(Chromeチーム)

#神話2: MPAはオフラインで動作できず、ネットワークが復旧したときに再試行するために更新を保存できない

前のセクションから、サービスワーカーはすべてをキャッシュして、アプリを完全にオフラインで動作させることができることがわかりました。しかし、オフラインのPOSTリクエストを保存し、インターネットが復旧したときに再試行したい場合はどうでしょうか?

workbox-offline-cfg.png

上記の構成Javascriptファイルは、2つの一般的なオフラインシナリオをサポートするようにWorkboxを設定する方法を示しています。ここでは、バックグラウンド同期が表示されます。ここでは、サービスワーカーにインターネットが原因で失敗したリクエストをキャッシュし、最大24時間再試行するように依頼します。

以下では、リクエストがオフラインで行われたときにトリガーされるオフラインキャッチハンドラーを定義します。HTMLまたはJSONレスポンスを含むテンプレートの部分を返すか、リクエスト入力に基づいてレスポンスを動的に構築できます。ここでは、可能性は無限です。

#神話3: MPAはページ遷移中に常に白いフラッシュが発生する

サービスワーカーのビデオでは、キャッシングとプリレンダリングを設定すれば、これが発生しないことがすでにわかりました。しかし、この神話は2019年までは一般的には真実ではありませんでした。2019年以降、ほとんどのブラウザは、次のページに必要なすべてのアセットが利用可能になるか、タイムアウトに達するまで、次の画面の描画を保留します。そのため、両方のページ間を遷移する際に白いフラッシュは発生しません。これは、同じオリジン/ドメイン内を移動する場合にのみ機能します。

chrome.comのペイント保持に関するドキュメント.

#神話4: MPAではドキュメント間の派手なページ遷移は不可能である。

シングルページアプリケーションフレームワークの登場により、ページ間のカスタム遷移がより一般的になりました。さまざまなナビゲーションスタイルの魅力は、ブラウザからページナビゲーションを完全に制御することから来ています。実際には、このような遷移は主にWeb開発会議の講演でのデモで人気がありました。

chrome.comのドキュメント間の遷移に関するドキュメント.

これは、特にRedditやHacker Newsのコメントセクションでは、シングルページアプリケーションの一般的な議論となっています。しかし、ブラウザは過去数年間、この問題をネイティブに解決しようと取り組んできました。Chrome 126では、ドキュメント間のビュートランジションが導入されました。つまり、CSSのみ、またはCSSとJavascriptを使用して、ページ間のこれらの派手なアニメーションと遷移を含めるようにMPAを構築できます。

私のお気に入りの点は、CSSのみで素敵なドキュメント間の遷移を作成できる可能性があることです。

cross-doc-transitions-css.png

Google Chromeの発表ページで詳しく知ることができます。

このリンクは、マルチページアプリケーションのデモをホストしています。ここでは、ドキュメント間のビュートランジションAPIを使用して、スタックベースのアニメーションをシミュレートする、基本的なサーバーレンダリングアプリケーションを試すことができます。

#誤解 5: htmx や MPA を使うと、すべてのユーザー操作はサーバー側で行われなければならない。

HTMX について議論される際に、この意見をよく耳にします。HTMX の位置付けが原因で、混乱が生じているのかもしれません。しかし、すべてをサーバーサイドで行う必要はありません。多くの HTMX や通常の MPA ユーザーは、必要に応じて Javascript、Alpine、Hyperscript を使い続けています。

堅牢なインタラクティビティが役立つ状況では、Web コンポーネントまたは任意の JavaScript フレームワーク (React、Angular など) を使用して、コンポーネントアイランドアーキテクチャを活用できます。そうすることで、アプリケーション全体を SPA にする代わりに、インタラクティビティを必要とするアプリケーションの部分にのみ、これらのフレームワークを活用できます。

上記の例は、APItoolkit の非常にインタラクティブな検索コンポーネントを示しています。これは、Web コンポーネントを作成するためのコンパイル不要のライブラリである lit-element を使用して実装された Web コンポーネントです。そのため、Web コンポーネント全体のイベントは 1 つの Javascript ファイルに収まります。

#誤解 6: DOM を直接操作するのは遅い。したがって、React/Virtual DOM を使用するのが最善である。

DOM の直接操作の速度は、ReactJS を構築し、仮想 DOM 技術を普及させた大きな動機でした。仮想 DOM 操作は直接 DOM 操作よりも高速になる場合がありますが、これは、多くの複雑な操作を実行し、ミリ秒単位で更新するアプリケーション、つまりパフォーマンスが顕著になる可能性のあるアプリケーションにのみ当てはまります。しかし、私たちのほとんどはそのようなソフトウェアを構築していません。

Svelte チームは、「仮想 DOM は純粋なオーバーヘッドである」というタイトルの素晴らしい記事 (「Virtual DOM is pure Overhead」) を書きました。ほとんどのアプリケーションでは仮想 DOM が重要でない理由がよりよく説明されているので、読んでみることをお勧めします。

#誤解 7: 些細なインタラクティビティごとに JavaScript を書く必要がある。

ブラウザ技術の進歩により、そもそも多くのクライアントサイド JavaScript を書くことを避けることができます。たとえば、Web 上の標準的なアクションは、ボタンのクリックまたはトグルに基づいて要素を表示/非表示にすることです。最近では、状態を追跡するために HTML 入力チェックボックスを使用するなど、CSS と HTML のみで要素を表示/非表示にすることができます。HTML ラベルをボタンとしてスタイル設定し、for="checkboxID" 属性を指定することで、ラベルをクリックするとチェックボックスが切り替わります。

<input id="published" class="hidden peer" type="checkbox"/>
<label for="published" class="btn">toggle content</label>

<div class="hidden peer-checked:block">
    Content to be toggled when label/btn is clicked
</div>

このようなチェックボックスと HTMX intersect を組み合わせることで、ボタンがクリックされたときにエンドポイントからコンテンツを取得できます。

<input id="published" class="peer" type="checkbox" name="status"/>
<div
        class="hidden peer-checked:block"
        hx-trigger="intersect once"
        hx-get="/log-item"
>Shell/Loading text etc
</div>

上記のクラスはすべて Vanilla Tailwind CSS クラスですが、CSS を手書きすることもできます。以下は、そのコードを使用してログエクスプローラーのログ項目を非表示または表示するビデオです。

#最後の誤解: *「適切な」*フロントエンドフレームワークがないと、クライアントサイドの Javascript はスパゲッティコードになり、保守できなくなる

これは真実かもしれませんし、そうでないかもしれません。

#どうでもいい。私はスパゲッティが好きだ。

Web の最も生産性の高い時代のいくつかは、PHP と JQuery のスパゲッティ時代だったと私は主張したいです。当時、多くのソフトウェアが構築され、今日私たちが知っている人気のあるインターネットブランドの多くが含まれていました。それらのほとんどはいわゆるスパゲッティコードとして構築され、製品を早期に出荷し、リファクタリングしてスパゲッティにならないのに十分な期間生き残るのに役立ちました。

#結論

この話の全体のポイントは、2024 年のブラウザでは多くのことが可能であることを示すことです。私たちが見ていない間に、ブラウザはギャップを埋め、シングルページアプリケーション革命から最高のアイデアを借りました。たとえば、Web コンポーネントは、シングルページアプリケーションから学んだ教訓のおかげで存在します。

そのため、今では、主にブラウザツール (HTML、CSS、場合によっては Javascript) を使用して、非常にインタラクティブな、オフラインでも動作する Web アプリケーションを構築でき、ユーザーエクスペリエンスを大きく犠牲にすることはありません。

ブラウザは長い道のりを歩んできました。チャンスを与えてください!

</>