implement Web Worker-based search to eliminate input lag
All checks were successful
CI / update (push) Successful in 1m13s

Replace synchronous DOM manipulation with Web Worker + Svelte reactive state for recipe search. This moves text normalization and filtering off the main thread, ensuring zero input lag while typing. Search now runs in parallel with UI rendering, improving performance significantly for 240+ recipes.

- Add search.worker.js for background search processing
- Update Search.svelte to use Web Worker with $state runes
- Update +page.svelte with reactive filtering based on worker results
- Add language-aware recipe data synchronization for proper English/German search
- Migrate to Svelte 5 event handlers (onsubmit, onclick)
This commit is contained in:
2025-12-31 14:09:16 +01:00
parent 5a55eb7cdd
commit 314d6225cc
3 changed files with 182 additions and 73 deletions

View File

@@ -5,7 +5,19 @@
import Card from '$lib/components/Card.svelte';
import Search from '$lib/components/Search.svelte';
let { data }: { data: PageData } = $props();
let current_month = new Date().getMonth() + 1
let current_month = new Date().getMonth() + 1;
// Search state
let matchedRecipeIds = $state(new Set());
let matchedCategories = $state(new Set());
let hasActiveSearch = $state(false);
// Handle search results from Search component
function handleSearchResults(ids, categories) {
matchedRecipeIds = ids;
matchedCategories = categories || new Set();
hasActiveSearch = ids.size < data.all_brief.length;
}
const isEnglish = $derived(data.lang === 'en');
const categories = $derived(isEnglish
@@ -49,20 +61,33 @@ h1{
<h1>{labels.title}</h1>
<p class=subheading>{labels.subheading}</p>
<Search lang={data.lang}></Search>
<Search lang={data.lang} recipes={data.all_brief} onSearchResults={handleSearchResults}></Search>
<MediaScroller title={labels.inSeason}>
{#each data.season as recipe}
<Card {recipe} {current_month} loading_strat={"eager"} do_margin_right={true} isFavorite={recipe.isFavorite} showFavoriteIndicator={!!data.session?.user} routePrefix="/{data.recipeLang}"></Card>
{/each}
</MediaScroller>
{#if true}
{@const seasonRecipes = data.season.filter(recipe =>
!hasActiveSearch || matchedRecipeIds.has(recipe._id)
)}
{#if seasonRecipes.length > 0}
<MediaScroller title={labels.inSeason}>
{#each seasonRecipes as recipe}
<Card {recipe} {current_month} loading_strat={"eager"} do_margin_right={true} isFavorite={recipe.isFavorite} showFavoriteIndicator={!!data.session?.user} routePrefix="/{data.recipeLang}"></Card>
{/each}
</MediaScroller>
{/if}
{/if}
{#each categories as category}
<MediaScroller title={category}>
{#each data.all_brief.filter(recipe => recipe.category == category) as recipe}
<Card {recipe} {current_month} do_margin_right={true} isFavorite={recipe.isFavorite} showFavoriteIndicator={!!data.session?.user} routePrefix="/{data.recipeLang}"></Card>
{/each}
</MediaScroller>
{@const categoryRecipes = data.all_brief.filter(recipe =>
recipe.category === category &&
(!hasActiveSearch || matchedRecipeIds.has(recipe._id))
)}
{#if categoryRecipes.length > 0}
<MediaScroller title={category}>
{#each categoryRecipes as recipe}
<Card {recipe} {current_month} do_margin_right={true} isFavorite={recipe.isFavorite} showFavoriteIndicator={!!data.session?.user} routePrefix="/{data.recipeLang}"></Card>
{/each}
</MediaScroller>
{/if}
{/each}
{#if !isEnglish}
<AddButton href="/rezepte/add"></AddButton>