implement category-based lazy loading to improve initial page load
All checks were successful
CI / update (push) Successful in 1m10s

Add Intersection Observer-based lazy loading for recipe categories to dramatically reduce initial render time. Categories render progressively as users scroll, reducing initial DOM from 240 cards to ~30-50 cards.

Performance improvements:
- First 2 categories render eagerly (~30-50 cards) for fast perceived load
- Remaining categories lazy-load 600px before entering viewport
- Categories render immediately during active search for instant results
- "In Season" section always renders first as hero content

Implementation:
- Add LazyCategory component with IntersectionObserver for vertical lazy loading
- Wrap MediaScroller categories with progressive loading logic
- Maintain scroll position with placeholder heights (300px per category)
- Keep search functionality fully intact with all 240 recipes searchable
- Horizontal lazy loading not implemented (IntersectionObserver doesn't work well with overflow-x scroll containers)
This commit is contained in:
2025-12-31 14:40:03 +01:00
parent 314d6225cc
commit 1182cfd239
2 changed files with 90 additions and 11 deletions

View File

@@ -4,6 +4,7 @@
import AddButton from '$lib/components/AddButton.svelte';
import Card from '$lib/components/Card.svelte';
import Search from '$lib/components/Search.svelte';
import LazyCategory from '$lib/components/LazyCategory.svelte';
let { data }: { data: PageData } = $props();
let current_month = new Date().getMonth() + 1;
@@ -68,25 +69,38 @@ h1{
!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>
<LazyCategory title={labels.inSeason} eager={true}>
{#snippet children()}
<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>
{/snippet}
</LazyCategory>
{/if}
{/if}
{#each categories as category}
{#each categories as category, index}
{@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>
<LazyCategory
title={category}
eager={index < 2 || hasActiveSearch}
estimatedHeight={300}
rootMargin="600px"
>
{#snippet children()}
<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>
{/snippet}
</LazyCategory>
{/if}
{/each}
{#if !isEnglish}