検索ページ(/search)パフォーマンス改善計画
開発blog-platform完了
検索ページ(/search)パフォーマンス改善計画
現状の問題
検索ページ(/search)の読み込みが遅い。原因は search.vue の以下の処理にある。
const { data: allArticles } = await useAsyncData('search-articles', () =>
queryCollection('pages').all()
)
queryCollection('pages').all() がbody ASTを含む全471記事の全フィールドを一括取得している。
何が起きているか
- サーバー側: SQLiteから471記事の全データ(本文ASTを含む)をクエリしてJSON化
- クライアント転送: その全データが
_payload.jsonとしてブラウザに送られる(約20MB) - クライアント側: 受け取った全データでFlexSearchインデックスを構築(
watchのimmediate: true)
FlexSearchのインデックス構築では、ASTからテキストを再帰的に抽出する extractTextFromAST も走る。471記事分のAST→テキスト変換がページ表示をブロックしている。
ビルドへの影響
ビルド時間にも影響していた。/search をプリレンダリング対象にしていたとき _payload.json が20MBに膨らみ、Server Buildに264.8秒かかっていた。プリレンダリングからの除外で138.1秒に短縮済み(48%削減)。
ただしランタイムの読み込み速度は改善されていない。
改善方針
全文検索をサーバーAPI側に移す。 クライアントには検索結果(タイトル・パス・スニペット)だけを返す。
現在のアーキテクチャ
ブラウザ → 全記事取得(20MB) → クライアントでFlexSearch構築 → 検索実行
改善後のアーキテクチャ
ブラウザ → 検索APIにクエリ送信 → サーバーでSQLite検索 → 結果のみ返却(数KB)
実装計画
ステップ1: サーバーAPIエンドポイントの作成
server/api/search.get.ts を作成する。
- クエリパラメータ
q(検索キーワード)、sort(並び順)、page(ページ番号)を受け取る - @nuxt/content のサーバー側APIでSQLiteを直接クエリ
title,description,path,publishedAt,tagsのみ返却(bodyは返さない)- 全文検索はSQLiteの
LIKEまたは FTS(Full-Text Search)を使う
// server/api/search.get.ts のイメージ
export default defineEventHandler(async (event) => {
const { q, sort = 'desc', page = 1, limit = 20 } = getQuery(event)
// @nuxt/content のサーバーAPI で検索
// body を除外して軽量なレスポンスを返す
})
ステップ2: search.vue のリファクタリング
queryCollection('pages').all()を削除- FlexSearchの依存を削除
- 検索入力時にサーバーAPIを呼び出す方式に変更
- デバウンス(300ms)は維持
// search.vue のイメージ
const { data: results } = await useFetch('/api/search', {
query: { q: debouncedQuery, sort: sortOrder, page: currentPage }
})
ステップ3: 本文スニペットの生成
- サーバー側で検索キーワード周辺のテキストを抽出してスニペットを返す
- クライアントではスニペットを表示するだけにする
- ハイライト処理はサーバー側で済ませるか、クライアントで軽量に実施
ステップ4: 一覧表示(検索キーワードなし)
検索キーワードが空のときは、記事一覧を返す。
select('title', 'description', 'path', 'publishedAt', 'tags')でbodyを除外- ページネーションはサーバー側で
.limit().offset()を使う - 転送量は20MB → 数十KB程度になる
期待される効果
| 指標 | 現状 | 改善後(見込み) |
|---|---|---|
| 初回payload | 約20MB | 数十KB |
| ページ表示速度 | 数秒〜十数秒 | 1秒以内 |
| クライアント側メモリ | FlexSearchインデックス + 全記事AST | 検索結果のみ |
| FlexSearch依存 | あり | 削除可能 |
注意点
- Cloudflare Pages (SSG) の場合、サーバーAPIは使えない。現在のプリセットは
cloudflare-pages-static - サーバーAPIを使うには、
cloudflare-pagesプリセット(SSR対応)への変更か、Cloudflare Workers経由でのD1利用が必要 - SSGのままで
select()によるbody除外 + クライアント側の軽量検索に留める方法もある
SSGのまま改善する場合
全文検索は諦めて、タイトル・説明文の部分一致検索に限定する。
const { data: allArticles } = await useAsyncData('search-articles', () =>
queryCollection('pages')
.select('title', 'description', 'path', 'publishedAt', 'tags')
.all()
)
この変更だけでbodyのAST転送がなくなり、payloadが大幅に減る。全文検索が不要ならFlexSearchも削除できる。
判断ポイント
| 方式 | メリット | デメリット |
|---|---|---|
SSGのまま .select() で軽量化 | 変更が小さい、プリセット変更不要 | 全文検索不可 |
| SSR化してサーバーAPI | 全文検索可能、最も高速 | プリセット変更が必要、D1設定 |
まず .select() による軽量化をSSGのまま行い、全文検索が必要になった時点でSSR化を検討する段階的アプローチが現実的。