perf(recipes/search): memoise per-recipe normalized search string
Every keystroke the filter rebuilt the lowercased + diacritic-stripped + soft-hyphen-stripped concat of name/description/tags per recipe. For a 200+ recipe catalogue that's a lot of regex work on the hot path. Cache the normalised string in a WeakMap keyed by the recipe object; first keystroke still pays the full cost, every subsequent one is a single indexOf per recipe. Picked client-side memoisation over the audit's suggested server-side `_searchKey` to avoid duplicating every recipe's text over the wire.
This commit is contained in:
@@ -14,7 +14,7 @@ Order = impact. Font items + app.html preload intentionally skipped.
|
||||
- [x] 8. Calendar payload trim — `yearDays` narrowed to `{iso, color}` (needle lookup only), new pre-filtered `feastDots` array carries feast-specific metadata. Also fixed a stray double `locals.session ?? (locals.session ?? …)` in both calendar page loaders.
|
||||
- [x] 9. History sessions endpoint — projection narrowed to exactly what SessionCard reads (drops notes, templates, mode, endTime, session-level gpsPreview); added `.lean()`.
|
||||
- [x] 10. `Cache-Control` headers: 8 h public on the shuffled recipe list endpoints (`all_brief`, `category/[c]`, `tag/[t]`, `icon/[i]`, `in_season/[m]`) — rand_array is seeded per UTC day, safe to share. 1 h public on distinct-value lists (`category`, `tag`, `icon`). 5 min public on recipe detail. `private 1h` on fitness `/exercises/filters`. Calendar page skipped (session serialised into layout HTML).
|
||||
- [ ] 11. Search — debounce 100 ms + server-side pre-normalized `_searchKey`
|
||||
- [x] 11. Search — debounce was already 100 ms. Instead of a server-side `_searchKey` (would duplicate text over the wire), memoise per-recipe normalized string in a `WeakMap` on the client — built lazily, reused across every subsequent keystroke.
|
||||
|
||||
## Features
|
||||
[x] on /fitness/measure, fill "Past measurements" in SSR only for the last 10 measurements. anything further should be fetched client side on mount to decreae initial page load time. use a "show more" button and paginate measurments.
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "homepage",
|
||||
"version": "1.46.24",
|
||||
"version": "1.46.25",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
@@ -122,6 +122,32 @@
|
||||
});
|
||||
}
|
||||
|
||||
/** @param {string} s */
|
||||
function normalizeSearchText(s) {
|
||||
return s.toLowerCase()
|
||||
.normalize('NFD')
|
||||
.replace(/\p{Diacritic}/gu, '')
|
||||
.replace(/­|/g, '');
|
||||
}
|
||||
|
||||
// Memoised normalized search string per recipe. Building it is the hot
|
||||
// path (NFD + regex replace for every recipe × every keystroke), so we
|
||||
// compute it once per recipe array and reuse across keystrokes. Shipping
|
||||
// a pre-normalized `_searchKey` from the server would duplicate the text
|
||||
// fields over the wire — this keeps the payload small and amortises the
|
||||
// cost on the client instead.
|
||||
/** @type {WeakMap<object, string>} */
|
||||
const searchIndex = new WeakMap();
|
||||
/** @param {any} recipe */
|
||||
function searchStringFor(recipe) {
|
||||
const cached = searchIndex.get(recipe);
|
||||
if (cached !== undefined) return cached;
|
||||
const raw = [recipe.name || '', recipe.description || '', ...(recipe.tags || [])].join(' ');
|
||||
const norm = normalizeSearchText(raw);
|
||||
searchIndex.set(recipe, norm);
|
||||
return norm;
|
||||
}
|
||||
|
||||
// Perform search directly (no worker)
|
||||
/** @param {string} query */
|
||||
function performSearch(query) {
|
||||
@@ -138,25 +164,11 @@
|
||||
}
|
||||
|
||||
// Normalize and split search query
|
||||
const searchText = query.toLowerCase().trim()
|
||||
.normalize('NFD')
|
||||
.replace(/\p{Diacritic}/gu, "");
|
||||
const searchTerms = searchText.split(" ").filter((/** @type {string} */ term) => term.length > 0);
|
||||
const searchText = normalizeSearchText(query.trim());
|
||||
const searchTerms = searchText.split(' ').filter((/** @type {string} */ term) => term.length > 0);
|
||||
|
||||
// Filter recipes by text
|
||||
const matched = filteredByNonText.filter((/** @type {any} */ recipe) => {
|
||||
// Build searchable string from recipe data
|
||||
const searchString = [
|
||||
recipe.name || '',
|
||||
recipe.description || '',
|
||||
...(recipe.tags || [])
|
||||
].join(' ')
|
||||
.toLowerCase()
|
||||
.normalize('NFD')
|
||||
.replace(/\p{Diacritic}/gu, "")
|
||||
.replace(/­|/g, ''); // Remove soft hyphens
|
||||
|
||||
// All search terms must match
|
||||
const searchString = searchStringFor(recipe);
|
||||
return searchTerms.every((/** @type {string} */ term) => searchString.includes(term));
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user