import-on-interaction について
クリックやスクロールなどの操作まで重要でないコードの読み込みを遅らせる、Web 開発者向けのガイドです。サードパーティウィジェットやアプリ機能の遅延読み込みを扱います。
主な機能
- 01動的 import() によるモジュールの遅延読み込み
- 02重い埋め込み機能向けの軽量なファサード
- 03React での lazy と Suspense の併用
- 04操作、スクロール、アイドル時の読み込み開始
活用シーン
- 01クリック後に動画プレーヤーやチャットを読み込む
- 02ログイン操作時に認証ライブラリを読み込む
- 03ボタンのクリック後に絵文字ピッカーを読み込む
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ patternsdev/ skills --skill import-on-interactionインストール後、Claude Code で/import-on-interactionを入力して読み込まれたか確認します。
詳細
操作時にインポート
目次
ページには、コンポーネントやリソースに関するコードやデータが含まれていることがありますが、すぐには必要ない場合もあります。たとえば、ページの一部で、ユーザーがクリックまたはスクロールしない限り表示されないユーザーインターフェースが該当します。これは、作成したさまざまな種類のファーストパーティーコードに当てはまりますが、通常はボタンをクリックしてメインインターフェースを表示する必要がある動画プレーヤーやチャットウィジェットなど、サードパーティー製ウィジェットにも当てはまります。
使用する場合
- 動画プレーヤーやチャットウィジェットなど、先行して読み込むとコストがかかるサードパーティー製ウィジェットがある場合に使用します
- ユーザーが実際に必要とするまで、重要度の低いコードの読み込みを遅らせるのに役立ちます
- First Input Delay(FID)と Time to Interactive(TTI)の改善に使用します
使用しない場合
- ページ読み込み時にリソースがすぐ必要で、ユーザー操作を待つ必要がない場合
- 操作後の読み込み遅延によってユーザー体験が明らかに損なわれる場合(代わりにプリフェッチ/プリロードを検討してください)
- 動的インポートのオーバーヘッドが読み込みを遅らせるメリットを上回る、小規模なモジュールの場合
手順
- 動的な
import()を使用して、ユーザー操作(クリック、ホバーなど)時にモジュールを読み込みます - 重いサードパーティー製埋め込みコンテンツには、軽量なプレースホルダーであるファサードを実装します
- ファーストパーティーコードでは、可能な場合、操作時のインポートよりプリフェッチを優先します
- レイテンシーを減らすため、ホバー時に必要なオリジンへの事前接続を検討します
- React でコンポーネント単位の操作時インポートを行うには、
SuspenseとともにReact.lazyを使用します
詳細
これらのリソースを先行して(つまり、すぐに)読み込むと、コストがかかる場合はメインスレッドをブロックする可能性があり、ユーザーがページのより重要な部分を操作できるようになるまでの時間が長くなります。これは、First Input Delay、Total Blocking Time、Time to Interactive などの操作可能になるまでの時間を測定する指標に影響する可能性があります。これらのリソースをすぐに読み込む代わりに、次のような適切なタイミングで読み込むことができます。
- ユーザーがコンポーネントを初めて操作するためにクリックしたとき
- コンポーネントがビューポート内にスクロールされて表示されたとき
- または、ブラウザーがアイドル状態になるまでコンポーネントの読み込みを遅らせるとき(requestIdleCallback を使用)。
リソースを読み込む方法は、大まかに次のとおりです。
- 先行読み込み - リソースをすぐに読み込む(スクリプトを読み込む通常の方法)
- 遅延読み込み(ルートベース) - ユーザーがルートまたはコンポーネントに移動したときに読み込む
- 遅延読み込み(操作時) - ユーザーが UI をクリックしたときに読み込む(例:チャットを表示)
- 遅延読み込み(ビューポート内) - ユーザーがコンポーネントに向かってスクロールしたときに読み込む
- プリフェッチ - 必要になる前に読み込む。ただし、重要なリソースの読み込み後に行う
- プリロード - より高い優先度で先行して読み込む
操作時に機能コードを遅延インポートするパターンは、さまざまな場面で使われています。たとえば Google Docs では、共有機能のスクリプト 500KB 分の読み込みを、ユーザーが操作するまで遅らせています。
操作時のインポートが適しているもう一つの例は、サードパーティー製ウィジェットの読み込みです。
ファサードを使用してサードパーティの UI を「偽装」して読み込む
サードパーティのスクリプトをインポートしていて、描画内容やコードの読み込みタイミングをあまり制御できない場合があります。操作時に読み込む方法を実装する手軽な選択肢の 1 つは、ファサードを使うことです。ファサードとは、画像やスクリーンショットなどを使って基本的な体験を再現する、負荷の大きいコンポーネントのシンプルな「プレビュー」または「プレースホルダー」です。これは Lighthouse チームがこのアイデアについて使っている用語です。
ユーザーが「プレビュー」(ファサード)をクリックすると、そのリソースのコードが読み込まれます。これにより、その機能を使わないユーザーが体験コストを負担する必要がなくなります。同様に、ファサードを使えば、ホバー時に必要なリソースへ事前接続できます。
動画プレーヤーの埋め込み
「ファサード」の良い例として、Paul Irish による YouTube Lite Embed があります。これは YouTube の動画 ID を受け取り、最小限のサムネイルと再生ボタンを表示するカスタム要素を提供します。要素をクリックすると、完全な YouTube 埋め込みコードが動的に読み込まれるため、再生ボタンをクリックしないユーザーは、その取得や処理のコストを負担しません。
Google のいくつかのサイトでは、同様の手法が本番環境で使われています。Android.com では、YouTube 動画プレーヤーの埋め込みを先に読み込む代わりに、偽のプレーヤーボタン付きのサムネイルをユーザーに表示します。ユーザーがクリックすると、モーダルが読み込まれ、完全版の YouTube 動画プレーヤーの埋め込みを使って動画が自動再生されます。
認証
アプリでは、クライアント側の JavaScript SDK を介したサービスへの認証をサポートする必要がある場合があります。これらは大きく、JavaScript の実行コストも高いことがあるため、ユーザーがログインしないのであれば、最初から先読みしないほうがよい場合があります。代わりに、ユーザーが「ログイン」ボタンをクリックしたときに認証ライブラリを動的にインポートすれば、初期読み込み中にメインスレッドの負荷を軽減できます。
チャットウィジェット
Calibre アプリは、同様のファサード手法を使って、Intercom ベースのライブチャットのパフォーマンスを30%改善しました。CSS と HTML だけを使った、高速に読み込まれる「偽」のライブチャットボタンを実装し、クリックすると Intercom のバンドルが読み込まれるようにしました。
Postmark は、顧客がたまにしか使わないにもかかわらず、ヘルプチャットウィジェットが常に先読みされていたと報告しています。このウィジェットは 314KB のスクリプトを読み込んでおり、ホームページ全体よりも大きなサイズでした。ユーザー体験を改善するため、ウィジェットを HTML と CSS で作った偽のレプリカに置き換え、本物はクリック時に読み込むようにしました。この変更により、操作可能になるまでの時間は 7.7 秒から 3.7 秒に短縮されました。
その他
Ne-digital は、ユーザーが「トップへスクロール」ボタンをクリックしたときに、ページの先頭へアニメーション付きでスクロールする React ライブラリを使用していました。このために react-scroll の依存関係を先読みする代わりに、ボタンの操作時に読み込むことで、約 7KB を節約しています。
handleScrollToTop() {
import('react-scroll').then(scroll => {
scroll.animateScroll.scrollToTop({
})
})
}
操作時にインポートするには?
Vanilla JavaScript
JavaScript では、動的な import() によってモジュールを遅延読み込みできます。Promise を返すため、適切に使えば非常に強力です。以下の例では、ボタンのイベントリスナー内で動的な import を使って lodash.sortby モジュールをインポートし、使用しています。
const btn = document.querySelector("button");
btn.addEventListener("click", (e) => {
e.preventDefault();
import("lodash.sortby")
.then((module) => module.default)
.then(sortInput()) // use the imported dependency
.catch((err) => {
console.log(err);
});
});
動的な import が使えるようになる前、または用途に合わない場合は、Promise ベースのスクリプトローダーを使ってページにスクリプトを動的に挿入する方法もありました。
const loginBtn = document.querySelector("#login");
loginBtn.addEventListener("click", () => {
const loader = new scriptLoader();
loader
.load(["//apis.google.com/js/client:platform.js?onload=showLoginScreen"])
.then(({ length }) => {
console.log(`${length} scripts loaded!`);
});
});
React
<MessageList>、<MessageInput>、そして emoji-mart(最小化・gzip 圧縮後で 98KB)を利用する <EmojiPicker> コンポーネントを備えたチャットアプリケーションを考えてみましょう。これらすべてのコンポーネントをページの初回読み込み時に一括で読み込むことはよくあります。
import MessageList from './MessageList';
import MessageInput from './MessageInput';
import EmojiPicker from './EmojiPicker';
const Channel = () => {
...
return (
<div>
<MessageList />
<MessageInput />
{emojiPickerOpen && <EmojiPicker />}
</div>
);
};
コード分割を使えば、こうした読み込み処理を比較的簡単に分割できます。React.lazy メソッドを使うと、動的インポートによって React アプリケーションをコンポーネント単位で簡単にコード分割できます。React.lazy 関数には、わずかな手間でアプリケーション内のコンポーネントを個別の JavaScript チャンクに分割できる仕組みが組み込まれています。これを Suspense コンポーネントと組み合わせると、読み込み状態にも対処できます。
import React, { lazy, Suspense } from 'react';
import MessageList from './MessageList';
import MessageInput from './MessageInput';
const EmojiPicker = lazy(
() => import('./EmojiPicker')
);
const Channel = () => {
...
return (
<div>
<MessageList />
<MessageInput />
{emojiPickerOpen && (
<Suspense fallback={<div>Loading...</div>}>
<EmojiPicker />
</Suspense>
)}
</div>
);
};
この考え方を発展させ、アプリケーションの初回読み込み時に先行して読み込むのではなく、<MessageInput> 内の絵文字アイコンがクリックされたときにだけ、Emoji Picker コンポーネントのコードをインポートできます。
import React, { useState, createElement } from "react";
import MessageList from "./MessageList";
import MessageInput from "./MessageInput";
import ErrorBoundary from "./ErrorBoundary";
const Channel = () => {
const [emojiPickerEl, setEmojiPickerEl] = useState(null);
const openEmojiPicker = () => {
import(/* webpackChunkName: "emoji-picker" */ "./EmojiPicker")
.then((module) => module.default)
.then((emojiPicker) => {
setEmojiPickerEl(createElement(emojiPicker));
});
};
const closeEmojiPickerHandler = () => {
setEmojiPickerEl(null);
};
return (
<ErrorBoundary>
<div>
<MessageList />
<MessageInput onClick={openEmojiPicker} />
{emojiPickerEl}
</div>
</ErrorBoundary>
);
};
Vue
Vue.js では、インタラクション時にインポートする同様のパターンを、いくつかの方法で実現できます。方法の一つは、動的インポートを関数でラップした () => import("./Emojipicker") を使って、Emojipicker Vue コンポーネントを動的にインポートすることです。通常、この方法では、コンポーネントのレンダリングが必要になったときに Vue.js が遅延読み込みします。
さらに、ユーザー操作を条件として遅延読み込みを行うこともできます。ピッカーの親要素である div に条件付きの v-if を設定し、ボタンのクリックで切り替えることで、ユーザーがクリックしたときに Emojipicker コンポーネントを条件付きで取得し、レンダリングできます。
<template>
<div>
<button @click="show = true">Load Emoji Picker</button>
<div v-if="show">
<emojipicker></emojipicker>
</div>
</div>
</template>
<script>
export default {
data: () => ({ show: false }),
components: {
Emojipicker: () => import("./Emojipicker"),
},
};
</script>
インタラクション時のインポートパターンは、Angularを含む、動的なコンポーネント読み込みに対応したほとんどのフレームワークやライブラリで実現できるはずです。
段階的読み込みの一環として行う、ファーストパーティーコードのインタラクション時インポート
インタラクション時のコード読み込みは、Flights や Photos のような大規模アプリケーションで Google が段階的読み込みを実現するうえでも重要な要素です。これを説明するために、Shubhie Panicker が以前紹介した例を見てみましょう。
ユーザーがインドのムンバイへの旅行を計画していて、料金を調べるために Google Hotels を訪れたとします。このインタラクションに必要なリソースはすべて先行して読み込めますが、ユーザーが目的地を選択していない場合、地図に必要な HTML/CSS/JS は不要です。
最も単純なダウンロードのシナリオとして、Google Hotels が素朴なクライアントサイドレンダリング(CSR)を使っているとします。すべてのコードが先行してダウンロードされ、処理されます。HTML、続いて JS、CSS を読み込み、その後データを取得し、すべてが揃ってから初めてレンダリングします。しかし、この場合、画面には何も表示されず、ユーザーは長時間待たされます。JavaScript と CSS の大部分が不要である可能性もあります。
次に、このエクスペリエンスをサーバーサイドレンダリング(SSR)に移行したとします。ユーザーはページをより早く、完全に表示できるようになります。これはすばらしいことですが、サーバーからデータが取得され、クライアント側のフレームワークでハイドレーションが完了するまでは、ページを操作できません。
SSR は改善策になりますが、ページが準備できているように見えるのに、何もタップできないという、ユーザーに違和感を与える状態を招くことがあります。ユーザーが苛立って何度も繰り返しクリックすることから、これを「連打クリック」と呼ぶこともあります。
Google Hotels の検索例に戻り、UI を少し詳しく見てみましょう。ユーザーが最適なホテルを探すために「その他のフィルター」をクリックすると、そのコンポーネントに必要なコードがダウンロードされます。
最初にダウンロードされるコードはごく最小限で、それ以降はユーザーの操作に応じて送信するコードが決まります。
操作に応じた遅延読み込みには、重要な点がいくつかあります。
- まず、ページをすばやく完全な状態で表示できるよう、最初に最小限のコードをダウンロードします。
- 次に、ユーザーがページの操作を始めたら、その操作に基づいてほかに読み込むコードを判断します。たとえば、「その他のフィルター」コンポーネントのコードを読み込みます。
- つまり、ユーザーが使う必要のなかったページ上の多くの機能については、コードがブラウザーに送信されません。
早い段階のクリックを取りこぼさないためには?
これらの Google チームが使用しているフレームワークスタックでは、最初の HTML チャンクに小さなイベントライブラリ(JSAction)が含まれているため、フレームワークのブートストラップが完了する前にクリックを検出できます。イベントは、次の 2 つの用途に使われます。
- ユーザーの操作に基づいてコンポーネントコードのダウンロードを開始する
- フレームワークのブートストラップが完了したときに、ユーザーの操作を再生する
ほかにも、次のタイミングでコンポーネントコードを読み込む方法が考えられます。
- アイドル状態になってから一定時間が経過したとき
- ユーザーが該当する UI/ボタン/行動喚起にマウスをホバーしたとき
- ブラウザーのシグナル(ネットワーク速度、データセーバーモードなど)に基づいて、積極性を段階的に調整するとき
データはどうなりますか?
ページのレンダリングに使用する初期データは、初期ページの SSR HTML に含まれ、ストリーミングされます。遅延読み込みされるデータは、どのコンポーネントに対応するかが分かった時点で、ユーザーの操作に基づいてダウンロードされます。
これにより、データの取得も CSS や JS と同様に機能する、操作時のインポートが実現します。コンポーネントは必要なコードとデータを把握しているため、そのリソースは常にリクエストひとつで取得できます。
この仕組みは、ビルド時にコンポーネントとその依存関係のグラフを作成して実現します。ウェブアプリケーションは、いつでもこのグラフを参照し、任意のコンポーネントに必要なリソース(コードとデータ)をすばやく取得できます。また、ルートではなくコンポーネント単位でコード分割できます。
上記の例の詳しい解説については、Elevating the Web Platform with the JavaScript Communityをご覧ください。
トレードオフ
コストのかかる処理をユーザーの操作時に近づけると、ページの初期読み込みを高速化できますが、この手法にもトレードオフがあります。
ユーザーがクリックしてからスクリプトの読み込みに時間がかかる場合はどうなりますか?
Google Hotels の例では、小さく細分化されたチャンクによって、コードやデータの取得と実行をユーザーが長時間待つ可能性を抑えています。一方、ほかのケースでは、大きな依存関係によって、ネットワークが遅いときに実際にこの問題が生じる可能性があります。
これを防ぐ方法のひとつは、読み込みをさらに細分化するか、ページの重要なコンテンツの読み込みが終わった後にこれらのリソースをプリフェッチすることです。実際にアプリでどの程度問題になるかを判断するため、この方法の影響を測定することをおすすめします。
ユーザーが操作する前に機能が使えないことについてはどうですか?
ファサードを使う場合のもうひとつのトレードオフは、ユーザーが操作する前は機能を使えないことです。たとえば、埋め込み動画プレーヤーではメディアを自動再生できません。そうした機能が重要な場合は、ユーザーが操作するまで読み込みを遅らせるのではなく、ユーザーがスクロールして表示範囲に入ったときにサードパーティーの iframe を遅延読み込みするなど、リソースを読み込む別の方法を検討するとよいでしょう。
インタラクティブな埋め込みを静的な形式に置き換える
操作時にインポートするパターンや段階的な読み込みについて説明しましたが、埋め込みを完全に静的なものにする方法はどうでしょうか?
埋め込みの最終的な描画内容は、ソーシャルメディアの投稿が初期ビューポートに表示される場合など、すぐに必要になることがあります。また、埋め込みによって2〜3MBのJavaScriptが読み込まれると、それ自体が課題になることもあります。埋め込みの内容がすぐに必要なため、遅延読み込みやファサードがあまり適さない場合があります。
パフォーマンスを最適化する場合は、埋め込みを見た目が似た静的な形式に完全に置き換え、よりインタラクティブなバージョン(例:元のソーシャルメディアの投稿)へのリンクを設ける方法があります。ビルド時に埋め込みのデータを取得し、静的なHTML形式に変換できます。
静的な代替手段はパフォーマンス向上に役立ちますが、多くの場合、独自の実装が必要になるため、選択肢を評価する際はこの点に留意してください。
まとめ
ファーストパーティのJavaScriptは、ウェブ上の最新のページで操作可能になるまでの時間に影響することがよくあります。しかし、メインスレッドを占有するファーストパーティまたはサードパーティの非クリティカルなJavaScriptによって、ネットワーク上で読み込みが遅れることもよくあります。
一般に、ドキュメントのヘッド内で同期的に実行されるサードパーティスクリプトは避け、ファーストパーティのJavaScriptの読み込みが完了してから、ブロックしないサードパーティスクリプトを読み込むことを目指してください。操作時にインポートするパターンを使えば、ユーザーがそれらのスクリプトによって動作するUIを必要とする可能性が高くなった時点まで、非クリティカルなリソースの読み込みを遅らせることができます。