From a625b407c0c8646f7f73d76534221c8a0a37b810 Mon Sep 17 00:00:00 2001 From: Bryandero98 Date: Sat, 5 Sep 2026 16:32:54 -0500 Subject: [PATCH 1/3] perf: build the js-search TF-IDF index lazily instead of on mount useDataList is shared by 8 components (IntegrationsGrid, blog listing, resources grid, news grid, Sistent components), and every one of them paid for a full TF-IDF index build over its entire dataset on mount, even though the overwhelming majority of page visits never search at all. On the homepage specifically, this meant indexing the full integrations catalog just to render a static 13-item preview with no search interaction (Lighthouse: 4,260ms desktop TBT, ~2.1s main-thread CPU). The index is now built on the first real keystroke, cached in a ref, and invalidated only when the underlying dataset changes - the search algorithm itself (js-search config, indexed fields, TF-IDF strategy) is untouched, so results are identical, just computed lazily. Verified live against `npm run develop:lite`: both the homepage and the full integrations listing page render without new console errors, and typing into the search box safely builds the lazy index and returns results with no exceptions. The lite build profile excludes the integrations MDX collection from its data layer entirely, so the catalog was empty during this run (0 results is expected there, not a regression) - a full BUILD_FULL_SITE=true build would be needed to verify against real integration data, and the real TBT/CPU numbers should be re-measured against the deployed site the same way the original issue measured them (PageSpeed Insights against a public URL, not a local dev server). Note: this commit skips the repo's pre-commit hook (--no-verify). The hook's lint-staged config runs eslint --max-warnings=0 against every staged *.js file, but eslint.config's own `ignores` list excludes src/utils/ entirely - so eslint's "file ignored" notice on this path is itself counted as a warning and trips --max-warnings=0. That's a pre-existing mismatch between .lintstagedrc.js and eslint.config (confirmed unrelated to this change) that would block any legitimate commit touching src/utils/*. Ran `npx eslint --no-ignore src/utils/usedataList.js` manually instead - clean, no errors. Closes #8019. Co-Authored-By: Claude Sonnet 5 Signed-off-by: Bryandero98 --- src/utils/usedataList.js | 47 ++++++++++++++++++++++------------------ 1 file changed, 26 insertions(+), 21 deletions(-) diff --git a/src/utils/usedataList.js b/src/utils/usedataList.js index e24495e883013c..b649b4b436f270 100644 --- a/src/utils/usedataList.js +++ b/src/utils/usedataList.js @@ -1,44 +1,49 @@ import * as JsSearch from "js-search"; -import { useState, useEffect } from "react"; +import { useState, useEffect, useRef } from "react"; const useDataList = ( data, setSearchQuery, searchQuery, paramsIndex, - paramSearch + paramSearch, ) => { const [dataList, setDataList] = useState(data); - const [search, setSearch] = useState([]); const [searchResults, setSearchResults] = useState([]); - const [isLoading, setIsLoading] = useState(true); const queryResults = searchQuery ? searchResults : dataList; - useEffect(() => { - rebuildIndex(); - }, [dataList]); + + // The TF-IDF index is the expensive part of a search (it walks and + // tokenizes every document up front) - built lazily on the first real + // keystroke instead of eagerly on mount, since most renders of this hook + // (embedded previews, limited-count sections, visitors who never search) + // never need it at all. Invalidated whenever the underlying dataset + // changes so a stale index is never served. + const searchIndexRef = useRef(null); useEffect(() => { - rebuildIndex(); - }, []); + searchIndexRef.current = null; + }, [dataList]); - const rebuildIndex = () => { - const dataToSearch = new JsSearch.Search(paramSearch); - dataToSearch.indexStrategy = new JsSearch.PrefixIndexStrategy(); - dataToSearch.sanitizer = new JsSearch.LowerCaseSanitizer(); - dataToSearch.searchIndex = new JsSearch.TfIdfSearchIndex(paramSearch); - dataToSearch.addIndex(paramsIndex); - dataToSearch.addIndex("body"); - dataToSearch.addDocuments(dataList); - setSearch(dataToSearch); - setIsLoading(false); + const getSearchIndex = () => { + if (!searchIndexRef.current) { + const dataToSearch = new JsSearch.Search(paramSearch); + dataToSearch.indexStrategy = new JsSearch.PrefixIndexStrategy(); + dataToSearch.sanitizer = new JsSearch.LowerCaseSanitizer(); + dataToSearch.searchIndex = new JsSearch.TfIdfSearchIndex(paramSearch); + dataToSearch.addIndex(paramsIndex); + dataToSearch.addIndex("body"); + dataToSearch.addDocuments(dataList); + searchIndexRef.current = dataToSearch; + } + return searchIndexRef.current; }; const searchData = (e) => { - const queryResult = search.search(e.target.value); + const queryResult = getSearchIndex().search(e.target.value); setSearchQuery(e.target.value.trim()); setSearchResults(queryResult); }; - return { queryResults, searchData, setDataList,dataList }; + return { queryResults, searchData, setDataList, dataList }; }; export default useDataList; From ee28dfc5c3e611c2fb2d7f01250e4ed33ab579b7 Mon Sep 17 00:00:00 2001 From: Bryandero98 Date: Sun, 6 Sep 2026 00:22:32 -0500 Subject: [PATCH 2/3] fix: recompute active search results when dataList changes CodeRabbit caught this on PR #8023: the effect that invalidates the cached TF-IDF index on a dataList change didn't also recompute searchResults, so queryResults kept showing matches from the old dataset until the next keystroke. Note: pre-commit lint-staged is skipped here because it fails on any change under src/utils/ regardless of content - eslint.config.js explicitly ignores that directory, but lint-staged's glob still tries to lint it with --max-warnings=0, and the resulting "file ignored" notice itself counts as a warning. Pre-existing repo config mismatch, unrelated to this change. Co-Authored-By: Claude Sonnet 5 Signed-off-by: Bryandero98 --- src/utils/usedataList.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/utils/usedataList.js b/src/utils/usedataList.js index b649b4b436f270..d8cf5fc09186f6 100644 --- a/src/utils/usedataList.js +++ b/src/utils/usedataList.js @@ -21,6 +21,14 @@ const useDataList = ( useEffect(() => { searchIndexRef.current = null; + // An active query's results were built from the old dataList - recompute + // them against the new one immediately instead of waiting for the next + // keystroke, otherwise queryResults keeps showing stale matches. + if (searchQuery) { + setSearchResults(getSearchIndex().search(searchQuery)); + } else { + setSearchResults([]); + } }, [dataList]); const getSearchIndex = () => { From 70b07b15a06b197607e533ee2703f7a42fa36c64 Mon Sep 17 00:00:00 2001 From: Bryandero98 Date: Sun, 6 Sep 2026 06:56:27 -0500 Subject: [PATCH 3/3] fix: skip building the search index for a blank query CodeRabbit flagged that a blank/whitespace-only keystroke still called getSearchIndex(), forcing the lazy TF-IDF build for nothing - defeating the point of building it lazily in the first place. --no-verify: the pre-commit hook fails on ANY change under src/utils/ regardless of content (eslint.config.js ignores that path, but lint-staged still targets it with --max-warnings=0, and ESLint's own "file ignored" notice counts as a warning) - confirmed pre-existing and approved by the user earlier this session for this same file. Co-Authored-By: Claude Sonnet 5 Signed-off-by: Bryandero98 --- src/utils/usedataList.js | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/utils/usedataList.js b/src/utils/usedataList.js index d8cf5fc09186f6..2318f6e1e68987 100644 --- a/src/utils/usedataList.js +++ b/src/utils/usedataList.js @@ -46,9 +46,15 @@ const useDataList = ( }; const searchData = (e) => { - const queryResult = getSearchIndex().search(e.target.value); - setSearchQuery(e.target.value.trim()); - setSearchResults(queryResult); + const trimmedQuery = e.target.value.trim(); + setSearchQuery(trimmedQuery); + // A blank query has nothing to search - skip it so an empty keystroke + // (or clearing the field) never forces the lazy index to build. + if (!trimmedQuery) { + setSearchResults([]); + return; + } + setSearchResults(getSearchIndex().search(trimmedQuery)); }; return { queryResults, searchData, setDataList, dataList };