以前のブログ記事「静的サイトに特化した検索ライブラリ Pagefind を試す」では、Pagefind を使用した本ブログ記事の検索機能の実装方法を紹介しました。
この Pagefind を使用するにあたり、Pagefind の UI 関連の JS ファイルを読み込んでから初期化(インスタンス作成)をする必要があります。
<script src="/pagefind/pagefind-ui.js"></script>
<div id="search"></div>
<script>
document.addEventListener('DOMContentLoaded', () => {
new PagefindUI({ element: "#search" });
});
</script>
ここで指定した JS はページを開いた直後に実行されるため、機能を使用せずにページを離れた場合でも、本来不要な JS ファイルのダウンロードと解析が実行されてしまいます。

この状態で Lighthouse のツリーマップを確認すると、Pagefind UI の JS のサイズが圧倒的に大きいことがわかります。
サイズが大きいのは Pagefind UI が重いというよりは、相対的にほかの JS が軽いためです。

ローカルサーバ上での測定ですが、JS 全体のファイルサイズ 69.3 KiB 1に対して pagefind-ui.js
が 65.2 KiB と、実に 94% もの割合を占めています。
これは、検索機能を使わないユーザに不要なファイルをダウンロードさせていることになるので、この問題を解消するために、動的インポート(Dynamic Import)を使用します。
動的インポート
見出し「動的インポート」JavaScript には静的なインポート(Static Import)と、動的なインポート(Dynamic Import)があります。以下のエクスポートされた JS ファイル(greetings.js
)をベースに、この 2 つのインポートの例を比較します。
export function hello(user) {
return `Hello, ${user}!`;
}
まず、静的インポート(Static Import)は import
キーワードで読み込みます。
import { hello } from './greetings.js';
console.log(hello('world')); // Hello, world!
この静的インポートでは、すぐさまファイルがインポートされ、スクリプトが実行可能な状態になります。つまり、ファイルを読み込むタイミングは選べません。
対して、以下の動的インポートでは、import()
構文を使用して非同期に指定できるため、読み込むタイミングを任意に指定できます。
Promise の then()
メソッドを使用する方法と、async/await
を使用する方法がありますが、この記事では後者の async/await
で進めます。
// then()
import('./greetings.js').then(({ hello }) => {
console.log(hello('world')); // Hello, world!
});
// async/await
(async () => {
const { hello } = await import('./greetings.js');
console.log(hello('world')); // Hello, world!
})();
ここで Pagefind UI に話を戻して、インポートするタイミングを考えます。
- A. 検索フォームがビューポートに出現したとき(
IntersectionObserver
) - B. 検索フォームにホバーしたとき(
mouseenter
) - C. テキストフィールドにフォーカスしたとき(
focus
)
「A」は、ファイルをインポートしてから、検索機能を使用するまでの時間の余裕があるので安定性はありますが、依然として不要なファイルを読み込ませてしまう問題が残ります。
「B」は「A」よりも具体的で、検索機能を使用するまでの時間の余裕も若干ありますが、マウス以外のデバイスの処理を別途検討しなければなりません。
「C」は、さらに具体的で多くの場面で改善を見込めるので、この「C. テキストフィールドにフォーカスしたとき(focus
)」で実装していきます。
ただ、検索機能を使用するまでの余裕がほとんどないので、読み込むファイルサイズが大きかったり、通信速度が十分ではない場合には、応答性の問題が発生する可能性があります。
以下は、先ほどの async/await
に反映したコードです。なお、エラーハンドリングをするための try...catch
文も追加しています。
(() => {
const search = document.getElementById('search');
if (!search) return;
const input = search.querySelector('input');
if (!input) return;
input.addEventListener('focus', async () => {
try {
// 動的インポート(Dynamic Import)
await import('/blog/pagefind/pagefind-ui.js');
// Pagefind を初期化する関数
initPagefind(search, input);
} catch (e) {
console.error(e);
}
}, { once: true });
})();
これで、テキストフィールドにフォーカスするまでは、Pagefind UI の JS ファイルが読み込まれることは無くなりました。
改めて Lighthouse のツリーマップを確認すると、JS 全体のファイルサイズは 69.3 KiB から 4.3 KiB まで大幅に下がりました。

もちろん、テキストフィールドにフォーカスすれば、結果的には同程度のサイズのリソースが読み込まれるのですが、訪問時の読み込み時間の短縮と、検索機能を使用しないユーザに対して、不要なファイルの読み込みを回避することができました。
プレースホルダーの用意と差し替え
見出し「プレースホルダーの用意と差し替え」2023/12/14 追記
前述のようにテキストフィールドにフォーカスしたときに動的インポートする場合、事前に対象となる <input>
要素が必要になります。
そのため、以下のようなコードで、プレースホルダーとなる空のテキストフィールドを用意しておき、Pagefind を初期化するタイミングで差し替えます。
<div id="search">
<!-- レイアウトシフトを防止するためのプレースホルダー -->
<div class="pagefind-ui">
<div class="pagefind-ui__form">
<!-- フォーカスの対象となるテキストフィールド -->
<input type="text" placeholder="ブログ記事を検索" class="pagefind-ui__search-input">
</div>
</div>
</div>
<script>
const initPagefind = (search, input) => {
// プレースホルダーを空にする
search.textContent = '';
// Pagefind 初期化
new PagefindUI({
element: '#search',
});
};
(() => {
const search = document.getElementById('search');
if (!search) return;
const input = search.querySelector('input');
if (!input) return;
input.addEventListener('focus', async () => {
try {
await import('/blog/pagefind/pagefind-ui.js');
initPagefind(search, input);
} catch (e) {
console.error(e);
}
}, { once: true });
})();
</script>
Pagefind では UI の JS ファイルを読み込んで初期化した後に、関連する HTML 要素が生成されるので、このようにプレースホルダーを用意しておくことで領域が確保され、CLS(Cumulative Layout Shift)の悪化を防ぐこともできます。
INP の改善
見出し「INP の改善」Google が提唱する重要なパフォーマンス指標である Core Web Vitals の 1 つである FID(First Input Delay)は、2024 年 3 月に INP(Interaction to Next Paint)に置き換わる予定です。
この INP は、ウェブページ内のインタラクティブな要素の応答性を評価しますが、ユーザが操作をしてから、視覚的なフィードバックが得られるまで(インタラクションを開始してから次のフレームが描画されるまで)の時間が対象となります。
また、FID の場合には最初のインタラクションのみが対象となりますが、INP ではページを訪問したときのすべてのインタラクションが対象となります。

特にサイズの大きな JS があると、パースやコンパイル時にメインスレッドを占有してしまい、ユーザの操作に対して遅延が発生したり、反応しないといった状況が発生してしまいます。
改善策としては、ページ読み込み時に優先度の低い JS ファイルを分割して遅延読み込みさせる Code Splitting(コード分割)の手法が有効ですが、先ほど取り上げた、動的インポート(Dynamic Import)がまさに代表例です。
Lighthouse の診断結果で「JavaScript の実行にかかる時間の低減」が指摘されたり、デベロッパーツールの Coverage で未使用の JS の割合が大きい場合には、試してみる価値があります。


参考文献
見出し「参考文献」本記事の作成にあたり、以下のウェブページを参考にしました。
脚注
-
国際規格においては、kB(キロバイト)は 1000 バイト、KiB(キビバイト)は 1024 バイトと定められていますが、大文字の KB(キロバイト)が 1024 バイトを表すケースもあり、これらの表記は混在しています(キロバイト | Wikipedia)。本記事においては、ツールで使用されている表記に統一しています。 ↩