[{"data":1,"prerenderedAt":903},["ShallowReactive",2],{"content-/search-page-performance-plan":3,"all-pages-for-dir":880,"related-/search-page-performance-plan":881,"og-image-/search-page-performance-plan":902},{"id":4,"title":5,"body":6,"category":862,"concepts":863,"description":864,"extension":865,"meta":866,"navigation":394,"ogImage":863,"path":867,"project_name":868,"published":869,"publishedAt":870,"seo":871,"stem":872,"tags":873,"todo":878,"unpublished":869,"updatedAt":863,"__hash__":879},"pages/2026-01/2026-01-29/search-page-performance-plan.md","検索ページ（/search）パフォーマンス改善計画",{"type":7,"value":8,"toc":841},"minimark",[9,13,17,30,128,139,143,177,184,187,196,199,202,208,211,219,222,228,231,235,241,288,414,418,434,521,525,536,540,543,565,568,635,638,660,664,667,782,785,788,831,837],[10,11,5],"h1",{"id":12},"検索ページsearchパフォーマンス改善計画",[14,15,16],"h2",{"id":16},"現状の問題",[18,19,20,21,25,26,29],"p",{},"検索ページ（",[22,23,24],"code",{},"/search","）の読み込みが遅い。原因は ",[22,27,28],{},"search.vue"," の以下の処理にある。",[31,32,37],"pre",{"className":33,"code":34,"language":35,"meta":36,"style":36},"language-ts shiki shiki-themes vitesse-light vitesse-light","const { data: allArticles } = await useAsyncData('search-articles', () =>\n  queryCollection('pages').all()\n)\n","ts","",[22,38,39,98,122],{"__ignoreMap":36},[40,41,44,48,52,56,59,62,65,68,72,76,79,83,87,89,92,95],"span",{"class":42,"line":43},"line",1,[40,45,47],{"class":46},"stQ0i","const ",[40,49,51],{"class":50},"shFtX","{",[40,53,55],{"class":54},"s4oTP"," data",[40,57,58],{"class":50},":",[40,60,61],{"class":54}," allArticles",[40,63,64],{"class":50}," }",[40,66,67],{"class":50}," =",[40,69,71],{"class":70},"sHkkW"," await",[40,73,75],{"class":74},"senZ8"," useAsyncData",[40,77,78],{"class":50},"(",[40,80,82],{"class":81},"sMJiu","'",[40,84,86],{"class":85},"sdGka","search-articles",[40,88,82],{"class":81},[40,90,91],{"class":50},",",[40,93,94],{"class":50}," ()",[40,96,97],{"class":50}," =>\n",[40,99,101,104,106,108,111,113,116,119],{"class":42,"line":100},2,[40,102,103],{"class":74},"  queryCollection",[40,105,78],{"class":50},[40,107,82],{"class":81},[40,109,110],{"class":85},"pages",[40,112,82],{"class":81},[40,114,115],{"class":50},").",[40,117,118],{"class":74},"all",[40,120,121],{"class":50},"()\n",[40,123,125],{"class":42,"line":124},3,[40,126,127],{"class":50},")\n",[18,129,130,133,134,138],{},[22,131,132],{},"queryCollection('pages').all()"," が",[135,136,137],"strong",{},"body ASTを含む全471記事の全フィールドを一括取得","している。",[140,141,142],"h3",{"id":142},"何が起きているか",[144,145,146,153,163],"ol",{},[147,148,149,152],"li",{},[135,150,151],{},"サーバー側",": SQLiteから471記事の全データ（本文ASTを含む）をクエリしてJSON化",[147,154,155,158,159,162],{},[135,156,157],{},"クライアント転送",": その全データが ",[22,160,161],{},"_payload.json"," としてブラウザに送られる（約20MB）",[147,164,165,168,169,172,173,176],{},[135,166,167],{},"クライアント側",": 受け取った全データでFlexSearchインデックスを構築（",[22,170,171],{},"watch"," の ",[22,174,175],{},"immediate: true","）",[18,178,179,180,183],{},"FlexSearchのインデックス構築では、ASTからテキストを再帰的に抽出する ",[22,181,182],{},"extractTextFromAST"," も走る。471記事分のAST→テキスト変換がページ表示をブロックしている。",[140,185,186],{"id":186},"ビルドへの影響",[18,188,189,190,192,193,195],{},"ビルド時間にも影響していた。",[22,191,24],{}," をプリレンダリング対象にしていたとき ",[22,194,161],{}," が20MBに膨らみ、Server Buildに264.8秒かかっていた。プリレンダリングからの除外で138.1秒に短縮済み（48%削減）。",[18,197,198],{},"ただしランタイムの読み込み速度は改善されていない。",[14,200,201],{"id":201},"改善方針",[18,203,204,207],{},[135,205,206],{},"全文検索をサーバーAPI側に移す。"," クライアントには検索結果（タイトル・パス・スニペット）だけを返す。",[140,209,210],{"id":210},"現在のアーキテクチャ",[31,212,217],{"className":213,"code":215,"language":216},[214],"language-text","ブラウザ → 全記事取得（20MB） → クライアントでFlexSearch構築 → 検索実行\n","text",[22,218,215],{"__ignoreMap":36},[140,220,221],{"id":221},"改善後のアーキテクチャ",[31,223,226],{"className":224,"code":225,"language":216},[214],"ブラウザ → 検索APIにクエリ送信 → サーバーでSQLite検索 → 結果のみ返却（数KB）\n",[22,227,225],{"__ignoreMap":36},[14,229,230],{"id":230},"実装計画",[140,232,234],{"id":233},"ステップ1-サーバーapiエンドポイントの作成","ステップ1: サーバーAPIエンドポイントの作成",[18,236,237,240],{},[22,238,239],{},"server/api/search.get.ts"," を作成する。",[242,243,244,259,262,281],"ul",{},[147,245,246,247,250,251,254,255,258],{},"クエリパラメータ ",[22,248,249],{},"q","（検索キーワード）、",[22,252,253],{},"sort","（並び順）、",[22,256,257],{},"page","（ページ番号）を受け取る",[147,260,261],{},"@nuxt/content のサーバー側APIでSQLiteを直接クエリ",[147,263,264,267,268,267,271,267,274,267,277,280],{},[22,265,266],{},"title",", ",[22,269,270],{},"description",[22,272,273],{},"path",[22,275,276],{},"publishedAt",[22,278,279],{},"tags"," のみ返却（bodyは返さない）",[147,282,283,284,287],{},"全文検索はSQLiteの ",[22,285,286],{},"LIKE"," または FTS（Full-Text Search）を使う",[31,289,291],{"className":33,"code":290,"language":35,"meta":36,"style":36},"// server/api/search.get.ts のイメージ\nexport default defineEventHandler(async (event) => {\n  const { q, sort = 'desc', page = 1, limit = 20 } = getQuery(event)\n\n  // @nuxt/content のサーバーAPI で検索\n  // body を除外して軽量なレスポンスを返す\n})\n",[22,292,293,299,330,389,396,402,408],{"__ignoreMap":36},[40,294,295],{"class":42,"line":43},[40,296,298],{"class":297},"sxvE3","// server/api/search.get.ts のイメージ\n",[40,300,301,304,307,310,312,315,318,321,324,327],{"class":42,"line":100},[40,302,303],{"class":70},"export",[40,305,306],{"class":70}," default",[40,308,309],{"class":74}," defineEventHandler",[40,311,78],{"class":50},[40,313,314],{"class":46},"async",[40,316,317],{"class":50}," (",[40,319,320],{"class":54},"event",[40,322,323],{"class":50},")",[40,325,326],{"class":50}," =>",[40,328,329],{"class":50}," {\n",[40,331,332,335,337,340,342,345,347,350,353,355,357,360,362,366,368,371,373,376,378,380,383,385,387],{"class":42,"line":124},[40,333,334],{"class":46},"  const ",[40,336,51],{"class":50},[40,338,339],{"class":54}," q",[40,341,91],{"class":50},[40,343,344],{"class":54}," sort",[40,346,67],{"class":50},[40,348,349],{"class":81}," '",[40,351,352],{"class":85},"desc",[40,354,82],{"class":81},[40,356,91],{"class":50},[40,358,359],{"class":54}," page",[40,361,67],{"class":50},[40,363,365],{"class":364},"sM54T"," 1",[40,367,91],{"class":50},[40,369,370],{"class":54}," limit",[40,372,67],{"class":50},[40,374,375],{"class":364}," 20",[40,377,64],{"class":50},[40,379,67],{"class":50},[40,381,382],{"class":74}," getQuery",[40,384,78],{"class":50},[40,386,320],{"class":54},[40,388,127],{"class":50},[40,390,392],{"class":42,"line":391},4,[40,393,395],{"emptyLinePlaceholder":394},true,"\n",[40,397,399],{"class":42,"line":398},5,[40,400,401],{"class":297},"  // @nuxt/content のサーバーAPI で検索\n",[40,403,405],{"class":42,"line":404},6,[40,406,407],{"class":297},"  // body を除外して軽量なレスポンスを返す\n",[40,409,411],{"class":42,"line":410},7,[40,412,413],{"class":50},"})\n",[140,415,417],{"id":416},"ステップ2-searchvue-のリファクタリング","ステップ2: search.vue のリファクタリング",[242,419,420,425,428,431],{},[147,421,422,424],{},[22,423,132],{}," を削除",[147,426,427],{},"FlexSearchの依存を削除",[147,429,430],{},"検索入力時にサーバーAPIを呼び出す方式に変更",[147,432,433],{},"デバウンス（300ms）は維持",[31,435,437],{"className":33,"code":436,"language":35,"meta":36,"style":36},"// search.vue のイメージ\nconst { data: results } = await useFetch('/api/search', {\n  query: { q: debouncedQuery, sort: sortOrder, page: currentPage }\n})\n",[22,438,439,444,479,517],{"__ignoreMap":36},[40,440,441],{"class":42,"line":43},[40,442,443],{"class":297},"// search.vue のイメージ\n",[40,445,446,448,450,452,454,457,459,461,463,466,468,470,473,475,477],{"class":42,"line":100},[40,447,47],{"class":46},[40,449,51],{"class":50},[40,451,55],{"class":54},[40,453,58],{"class":50},[40,455,456],{"class":54}," results",[40,458,64],{"class":50},[40,460,67],{"class":50},[40,462,71],{"class":70},[40,464,465],{"class":74}," useFetch",[40,467,78],{"class":50},[40,469,82],{"class":81},[40,471,472],{"class":85},"/api/search",[40,474,82],{"class":81},[40,476,91],{"class":50},[40,478,329],{"class":50},[40,480,481,485,488,490,493,496,498,500,502,505,507,509,511,514],{"class":42,"line":124},[40,482,484],{"class":483},"sz8Xr","  query",[40,486,487],{"class":50},": { ",[40,489,249],{"class":483},[40,491,492],{"class":50},": ",[40,494,495],{"class":54},"debouncedQuery",[40,497,267],{"class":50},[40,499,253],{"class":483},[40,501,492],{"class":50},[40,503,504],{"class":54},"sortOrder",[40,506,267],{"class":50},[40,508,257],{"class":483},[40,510,492],{"class":50},[40,512,513],{"class":54},"currentPage",[40,515,516],{"class":50}," }\n",[40,518,519],{"class":42,"line":391},[40,520,413],{"class":50},[140,522,524],{"id":523},"ステップ3-本文スニペットの生成","ステップ3: 本文スニペットの生成",[242,526,527,530,533],{},[147,528,529],{},"サーバー側で検索キーワード周辺のテキストを抽出してスニペットを返す",[147,531,532],{},"クライアントではスニペットを表示するだけにする",[147,534,535],{},"ハイライト処理はサーバー側で済ませるか、クライアントで軽量に実施",[140,537,539],{"id":538},"ステップ4-一覧表示検索キーワードなし","ステップ4: 一覧表示（検索キーワードなし）",[18,541,542],{},"検索キーワードが空のときは、記事一覧を返す。",[242,544,545,551,562],{},[147,546,547,550],{},[22,548,549],{},"select('title', 'description', 'path', 'publishedAt', 'tags')"," でbodyを除外",[147,552,553,554,557,558,561],{},"ページネーションはサーバー側で ",[22,555,556],{},".limit()"," ",[22,559,560],{},".offset()"," を使う",[147,563,564],{},"転送量は20MB → 数十KB程度になる",[14,566,567],{"id":567},"期待される効果",[569,570,571,587],"table",{},[572,573,574],"thead",{},[575,576,577,581,584],"tr",{},[578,579,580],"th",{},"指標",[578,582,583],{},"現状",[578,585,586],{},"改善後（見込み）",[588,589,590,602,613,624],"tbody",{},[575,591,592,596,599],{},[593,594,595],"td",{},"初回payload",[593,597,598],{},"約20MB",[593,600,601],{},"数十KB",[575,603,604,607,610],{},[593,605,606],{},"ページ表示速度",[593,608,609],{},"数秒〜十数秒",[593,611,612],{},"1秒以内",[575,614,615,618,621],{},[593,616,617],{},"クライアント側メモリ",[593,619,620],{},"FlexSearchインデックス + 全記事AST",[593,622,623],{},"検索結果のみ",[575,625,626,629,632],{},[593,627,628],{},"FlexSearch依存",[593,630,631],{},"あり",[593,633,634],{},"削除可能",[14,636,637],{"id":637},"注意点",[242,639,640,646,653],{},[147,641,642,643],{},"Cloudflare Pages (SSG) の場合、サーバーAPIは使えない。現在のプリセットは ",[22,644,645],{},"cloudflare-pages-static",[147,647,648,649,652],{},"サーバーAPIを使うには、",[22,650,651],{},"cloudflare-pages"," プリセット（SSR対応）への変更か、Cloudflare Workers経由でのD1利用が必要",[147,654,655,656,659],{},"SSGのままで ",[22,657,658],{},"select()"," によるbody除外 + クライアント側の軽量検索に留める方法もある",[140,661,663],{"id":662},"ssgのまま改善する場合","SSGのまま改善する場合",[18,665,666],{},"全文検索は諦めて、タイトル・説明文の部分一致検索に限定する。",[31,668,670],{"className":33,"code":669,"language":35,"meta":36,"style":36},"const { data: allArticles } = await useAsyncData('search-articles', () =>\n  queryCollection('pages')\n    .select('title', 'description', 'path', 'publishedAt', 'tags')\n    .all()\n)\n",[22,671,672,706,720,770,778],{"__ignoreMap":36},[40,673,674,676,678,680,682,684,686,688,690,692,694,696,698,700,702,704],{"class":42,"line":43},[40,675,47],{"class":46},[40,677,51],{"class":50},[40,679,55],{"class":54},[40,681,58],{"class":50},[40,683,61],{"class":54},[40,685,64],{"class":50},[40,687,67],{"class":50},[40,689,71],{"class":70},[40,691,75],{"class":74},[40,693,78],{"class":50},[40,695,82],{"class":81},[40,697,86],{"class":85},[40,699,82],{"class":81},[40,701,91],{"class":50},[40,703,94],{"class":50},[40,705,97],{"class":50},[40,707,708,710,712,714,716,718],{"class":42,"line":100},[40,709,103],{"class":74},[40,711,78],{"class":50},[40,713,82],{"class":81},[40,715,110],{"class":85},[40,717,82],{"class":81},[40,719,127],{"class":50},[40,721,722,725,728,730,732,734,736,738,740,742,744,746,748,750,752,754,756,758,760,762,764,766,768],{"class":42,"line":124},[40,723,724],{"class":50},"    .",[40,726,727],{"class":74},"select",[40,729,78],{"class":50},[40,731,82],{"class":81},[40,733,266],{"class":85},[40,735,82],{"class":81},[40,737,91],{"class":50},[40,739,349],{"class":81},[40,741,270],{"class":85},[40,743,82],{"class":81},[40,745,91],{"class":50},[40,747,349],{"class":81},[40,749,273],{"class":85},[40,751,82],{"class":81},[40,753,91],{"class":50},[40,755,349],{"class":81},[40,757,276],{"class":85},[40,759,82],{"class":81},[40,761,91],{"class":50},[40,763,349],{"class":81},[40,765,279],{"class":85},[40,767,82],{"class":81},[40,769,127],{"class":50},[40,771,772,774,776],{"class":42,"line":391},[40,773,724],{"class":50},[40,775,118],{"class":74},[40,777,121],{"class":50},[40,779,780],{"class":42,"line":398},[40,781,127],{"class":50},[18,783,784],{},"この変更だけでbodyのAST転送がなくなり、payloadが大幅に減る。全文検索が不要ならFlexSearchも削除できる。",[14,786,787],{"id":787},"判断ポイント",[569,789,790,803],{},[572,791,792],{},[575,793,794,797,800],{},[578,795,796],{},"方式",[578,798,799],{},"メリット",[578,801,802],{},"デメリット",[588,804,805,820],{},[575,806,807,814,817],{},[593,808,809,810,813],{},"SSGのまま ",[22,811,812],{},".select()"," で軽量化",[593,815,816],{},"変更が小さい、プリセット変更不要",[593,818,819],{},"全文検索不可",[575,821,822,825,828],{},[593,823,824],{},"SSR化してサーバーAPI",[593,826,827],{},"全文検索可能、最も高速",[593,829,830],{},"プリセット変更が必要、D1設定",[18,832,833,834,836],{},"まず ",[22,835,812],{}," による軽量化をSSGのまま行い、全文検索が必要になった時点でSSR化を検討する段階的アプローチが現実的。",[838,839,840],"style",{},"html pre.shiki code .stQ0i, html code.shiki .stQ0i{--shiki-default:#AB5959;--shiki-dark:#AB5959}html pre.shiki code .shFtX, html code.shiki .shFtX{--shiki-default:#999999;--shiki-dark:#999999}html pre.shiki code .s4oTP, html code.shiki .s4oTP{--shiki-default:#B07D48;--shiki-dark:#B07D48}html pre.shiki code .sHkkW, html code.shiki .sHkkW{--shiki-default:#1E754F;--shiki-dark:#1E754F}html pre.shiki code .senZ8, html code.shiki .senZ8{--shiki-default:#59873A;--shiki-dark:#59873A}html pre.shiki code .sMJiu, html code.shiki .sMJiu{--shiki-default:#B5695977;--shiki-dark:#B5695977}html pre.shiki code .sdGka, html code.shiki .sdGka{--shiki-default:#B56959;--shiki-dark:#B56959}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sxvE3, html code.shiki .sxvE3{--shiki-default:#A0ADA0;--shiki-dark:#A0ADA0}html pre.shiki code .sM54T, html code.shiki .sM54T{--shiki-default:#2F798A;--shiki-dark:#2F798A}html pre.shiki code .sz8Xr, html code.shiki .sz8Xr{--shiki-default:#998418;--shiki-dark:#998418}",{"title":36,"searchDepth":100,"depth":100,"links":842},[843,847,851,857,858,861],{"id":16,"depth":100,"text":16,"children":844},[845,846],{"id":142,"depth":124,"text":142},{"id":186,"depth":124,"text":186},{"id":201,"depth":100,"text":201,"children":848},[849,850],{"id":210,"depth":124,"text":210},{"id":221,"depth":124,"text":221},{"id":230,"depth":100,"text":230,"children":852},[853,854,855,856],{"id":233,"depth":124,"text":234},{"id":416,"depth":124,"text":417},{"id":523,"depth":124,"text":524},{"id":538,"depth":124,"text":539},{"id":567,"depth":100,"text":567},{"id":637,"depth":100,"text":637,"children":859},[860],{"id":662,"depth":124,"text":663},{"id":787,"depth":100,"text":787},"dev",null,"全記事のbody ASTを一括取得している検索ページの読み込み速度を改善するための実装計画","md",{},"/search-page-performance-plan","blog-platform",false,"2026-01-29T00:00:00.000Z",{"title":5,"description":864},"2026-01/2026-01-29/search-page-performance-plan",[874,875,876,877],"performance","nuxt","search","refactoring","done","GAz9i7rCML_eRBd9DxN5UeGwbTStl12TeVNhcPiOn6w",[],[882,886,890,894,898],{"title":883,"path":884,"publishedAt":885},"2026年1月12日の開発日記 - Nuxt 4移行とDBリファクタリング","/2026-01-12-diary","2026-01-12T00:00:00.000Z",{"title":887,"path":888,"publishedAt":889},"Nuxt 4 + @nuxt/content の開発サーバー起動が遅い原因と対策","/nuxt-dev-startup-time","2026-01-10T00:00:00.000Z",{"title":891,"path":892,"publishedAt":893},"Nuxtビルド最適化：341ページを137秒でSSG","/nuxt-devtools-optimization","2026-01-02T00:00:00.000Z",{"title":895,"path":896,"publishedAt":897},"財務データ全量表示の遅延読み込み設計","/raw-metrics-lazy-loading","2025-12-18T00:00:00.000Z",{"title":899,"path":900,"publishedAt":901},"Nuxt SSRエラー対策: データ分割とClientOnly","/nuxt-client-only-guide","2025-12-15T00:00:00.000Z","https://log.eurekapu.com/og/blog/search-page-performance-plan.png?v=2026-01-29T00%3A00%3A00.000Z&title=%E6%A4%9C%E7%B4%A2%E3%83%9A%E3%83%BC%E3%82%B8%EF%BC%88%2Fsearch%EF%BC%89%E3%83%91%E3%83%95%E3%82%A9%E3%83%BC%E3%83%9E%E3%83%B3%E3%82%B9%E6%94%B9%E5%96%84%E8%A8%88%E7%94%BB&author=Kei%20Komatsu&sig=e68a3c017f638a58",1784786422498]