『検索機能』の実装をJavaScript、React(Nextjs)で比較

2025年8月29日

ブログ内の検索機能をJavaScript(Vanilla JS)、React(Nextjs)それぞれで実装してみます。

https://www.masato-tech-blog.com/

筆者はReact(Nextjs)から入ったためVanilla JSの経験が乏しく、
あえてVanilla JSを使うことで、React(Nextjs)を使えることのメリットを実感するのが目的となります。

目次

  1. 全体の比較
  2. JavaScript(Vanilla JS)での実装
  3. Reactでの実装
  4. 結果整理
  5. さいごに

1. 全体の比較

<全体のイメージ>

Vanilla JS版: jsスクリプトで実装 → 検索実行 → 結果をNext.jsに渡す

React版: Hooksで実装 → 検索実行 → 結果をNext.jsに渡す

Next.js版: SSG/SSR + 最適化機能で実装 → 検索実行 →同一コンポーネント内で完結

機能Vanilla JSReactNext.js
検索実行DOM操作 + イベントuseState + onClickuseMemo + 自動実行
結果表示外部コンポーネント外部コンポーネント同一 + 動的インポート
URL管理なしなしrouter.push
パフォーマンス生JavaScriptReact最適化メモ化 + コード分割
SEO対応なしなしURL
同期ブラウザ履歴なしなし自動対応

<パフォーマンス比較>

  • 初期表示速度
    Vanilla JS: HTML読み込み → JS読み込み → CSS読み込み → API → 表示
    React: HTML読み込み → 即座に表示(データは後から)
    Next.js: HTML読み込み → データ込みで即座に表示

  • 検索実行速度(実測ではなく、イメージ例)
    Vanilla JS: 500ms (ネットワーク依存)
    React: 100ms + データ読み込み時間
    Next.js: 50ms (ビルド時最適化)

<技術選択の判断基準>
簡単な機能 → Vanilla JS

UI状態管理が複雑 → React

SEO + パフォーマンス重視 → Next.js

Next.jsの方が初期学習コストは高いですが、パフォーマンス面では圧倒的に有利


2. JavaScriptでの実装例

前提として、ファイルルーティングはブログサイトのベースとなっているNextjs14のappルーターを使用します。
そのため本当のVanilla JSと差異がある点はご承知おきください。

全体の流れとしては下記のようになります。

  • 検索用の元記事データをJSONファイルとして生成(app/publicフォルダ以下に保存)
     ※この作業はSSGでビルドの際に実行されることになります。
  • 検索コンポーネント内でJSスクリプトファイルを読み込み(検索ボタン押下で検索結果ページへリダイレクト)
  • 検索結果ページで検索処理を実行&検索結果を表示

以下にコードを貼り付けておきますが、最低限の機能しか実装していないのでだいぶ雑です。。。(エラーハンドリングもなし)

検索用のコンポーネント

alt text

SearchJavascript.tsx

// 検索用の元記事データをJSONファイルとして生成
export async function createJsonPosts() {
  const allPostsData = await fetchAllGithubArticles();

  // public/data/search.jsonとして保存
  const publicDir = path.join(process.cwd(), "public", "data");
  console.log("publicDir", publicDir);

  fs.writeFileSync(
    path.join(publicDir, "search.json"),
    JSON.stringify(allPostsData, null, 2)
  );
}

(async () => {
  await createJsonPosts();
})();

// 検索コンポーネント内でJSスクリプトファイルを読み込み
const searchJavascript = ({ posts }: Props) => {
  // console.log("posts of SearchJS", posts);

  return (
    <>
      <h2>Vanilla JSによる検索</h2>
      <div className="flex w-auto h-8 mb-4">
        <div id="javascript-search-container" className="w-full shadow-md">
          検索窓確認用
        </div>

        {/* JavaScript実行(スクリプトファイルの読み込み) */}
        <Script src="/js/javascript-search.js" strategy="afterInteractive" />
      </div>
    </>
  );
};

検索結果ページ

alt text

SearchResults.tsx

// 検索結果ページで検索処理を実行&検索結果を表示
const SearchReasults = ({ filterdPosts }: any) => {
  console.log("filterdPosts", filterdPosts);
  return (
    <div className="flex w-full mx-auto">
      <div id="search-results-js" className="w-full shadow-md">
        JSによる検索結果
      </div>

      {/* JavaScript実行 */}
      <Script src="/js/javascript-search.js" strategy="afterInteractive" />
    </div>
  );
};

JSスクリプトファイル

javascript-search.js

(function () {
  let posts = [];

  function loadCSS() {
    const link = document.createElement("link");
    link.rel = "stylesheet";
    link.href = "/css/javascript-search.css";
    document.head.appendChild(link);
    console.log("CSS loaded");
  }

  async function loadPosts() {
    const res = await fetch("/data/search.json");
    const posts = await res.json();
    console.log("posts", posts);
    return posts;
  }

  // 検索バーでの初期化
  function initSearchBar() {
    loadCSS();

    const container = document.getElementById("javascript-search-container");
    if (!container) return;

    // 検索UIのHTML構造を動的生成
    container.innerHTML = `
        <div class="vanilla-search-form">
          <input type="text" id="search-input" placeholder="検索...">
          <button id="search-button-js" type="button">検索</button>
        </div>
        `;

    setupSearchBarEvents();
  }

  // 検索実行&結果表示ページでの初期化
  async function initSearchPage() {
    loadCSS();

    // URLパラメータから検索クエリを取得
    const urlParams = new URLSearchParams(window.location.search);
    const query = urlParams.get("q");
    console.log("perform query:", query);

    const result = await loadPosts();
    posts = result;
    console.log("loadPosts完了時のposts:", posts.length);
    performSearch(posts, query);
  }

  // 検索ボタンを押した時点でリダイレクト
  function clickSearch() {
    const searchInput = document.getElementById("search-input");

    const query = searchInput.value.toLowerCase().trim();
    if (query) {
      // 検索結果ページにリダイレクト

      console.log("query", query);
      location.href = `/search?q=${encodeURIComponent(query)}`;
    }
  }

  // 検索バーでのイベント設定
  function setupSearchBarEvents() {
    const searchButton = document.getElementById("search-button-js");

    // イベント設定
    if (searchButton) {
      searchButton.addEventListener("click", clickSearch);
    }
  }

  // 検索実行関数
  async function performSearch(posts, query) {
    console.log("=== performSearch開始 ===");
    console.log("posts配列:", posts);
    console.log("検索query:", query);

    const filtered = posts.filter((post) =>
      post.title.toLowerCase().includes(query)
    );
    console.log("filtered", filtered);

    const searchResults = document.getElementById("search-results-js");
    if (!searchResults) return;

    // 検索結果のHTML構造を動的生成
    const resultsHTML = filtered
      .map((post) => `<h3>${post.title}</h3>`)
      .join("");

    searchResults.innerHTML = `
        <div>
          <div id="search-results-js">${resultsHTML}</div>
        </div>
        `;
  }

  // DOMが完全に読み込まれてから実行
  if (document.readyState === "loading") {
    document.addEventListener("DOMContentLoaded", initJavaScriptSearch);
  } else {
    //すでに読み込み済みの場合は即実行
    initSearchBar();
    initSearchPage();
  }
})();

3. Reactでの実装

最後にReactでの実装画面です。
実際のブログサイトではこちらを採用しています。
検索結果画面も、現在のブログの表示にレイアウトを合わせました。

(補足)
こちらは動的ページなのでSSRで実装したのですが、
Google AnalyticsのAPIがNode.js標準で、Cloudflare pagesのruntime edgeでは使えないため、人気記事ランキングのコンポーネントは除外しないといけなかったです。

alt text

// 検索Boxコンポーネント
Search.tsx

"use client";

import React, { useState } from "react";
import SearchIcon from "@mui/icons-material/Search";

const Search = () => {
  const [inputSearch, setInputSearch] = useState<string>("");

  // 検索ボックスの内容を取得
  const handleInputChange = (
    e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>
  ) => {
    setInputSearch(e.target.value);
    console.log("inputSearch:", inputSearch);
  };

  const handleClickSearch = () => {
    const searchWord = inputSearch.toLocaleLowerCase().trim();
    // リダイレクト処理
    window.location.href = `/search?q=${encodeURIComponent(searchWord)}`;
  };

  return (
    <div id="search-container" className="flex w-full h-8 mb-4">
      <input
        id="search-input"
        className="w-full shadow-md mr-2"
        type="text"
        placeholder=" サイト内検索"
        value={inputSearch}
        onChange={handleInputChange}
      />

      <button
        id="search-icon"
        className="w-10 bg-white shadow-md border-gray-700"
        onClick={handleClickSearch}
      >
        <SearchIcon />
      </button>
    </div>
  );
};

export default Search;
// 検索処理&検索結果コンポーネント(検索処理の部分のみを抜粋)
SearchResults.tsx

"use client";

// import Script from "next/script";
import React from "react";
import { useSearchParams } from "next/navigation";
import Link from "next/link";
import { AccessTime, Folder, Update } from "@mui/icons-material";
import SafeHtml from "app/utils/sanitizeHtml";

const SearchReasults = ({ allPostsData }: any) => {
  // URLパラメータから検索クエリを取得
  const urlParams = useSearchParams();
  const query = urlParams.get("q");

  // 検索クエリパラメータと一致する記事を抽出
  const filteredPosts: any = query
    ? allPostsData.filter((post: any) => {
        const title = post.title?.toLowerCase() || "";
        const content = post.content?.toLowerCase() || "";
        const searchQuery = query.toLowerCase();

        return title.includes(searchQuery) || content.includes(searchQuery);
      })
    : null;

4. 結論的なもの。実際にやってみて、自身の理解内容の整理

元の理解
・JSとReactでは検索ロジックに差が出る(Reactのライブラリなどが使える)、と思っていた。

学習後の理解
・検索ロジック部分では差はでない(filterメソッドは同じ)
・JSはスクリプトファイルの読み込み(CSS含む)が必要なため、表示時間がReactやNext.jsに比べて遅くなる
・今回は単純な機能のため、Reactの状態管理などの部分でメリットが実感しにくい

さいごに

私のようにReactでは書けるけど、JavaScriptオンリーでは書けない、という人も結構いるのではないでしょうか?(・・・いるかな?)

ReactやNext.jsが裏でどのように動いているのか理解するために、ベースになっている技術を学び直すことも大事だと思いトライしてみました。


記事一覧に戻る