From eb3604f9ea6aba804d9d564dce87d9ac59631c7f Mon Sep 17 00:00:00 2001 From: Alexander Bocken Date: Thu, 23 Apr 2026 15:00:37 +0200 Subject: [PATCH] perf: drop redundant JSON.parse(JSON.stringify()) in recipe API MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Every recipe list endpoint wrapped its result in `JSON.parse(JSON.stringify(...))` before handing it to `json()`, which then serialises again — a full extra stringify+parse cycle per response. `lean()` already returns plain objects and ObjectIds/Dates serialise correctly through `json()`'s single `JSON.stringify`, so the extra round trip was pure waste. Removed from the 9 output-side call sites (all_brief, category, category/[cat], tag, tag/[tag], icon, icon/[icon], in_season/[month], search, favorites/recipes, offline-db, translate/untranslated). Kept the two deep-clone-before-mutation usages in items/[name] and json-ld/[name] — those are load-bearing. Shuffle stays server-side: moving it to the client would need a hero preload + hydration rework that's bigger than a perf tweak. --- TODO.md | 2 +- package.json | 2 +- .../api/[recipeLang=recipeLang]/favorites/recipes/+server.ts | 4 ++-- .../api/[recipeLang=recipeLang]/items/all_brief/+server.ts | 2 +- .../api/[recipeLang=recipeLang]/items/category/+server.ts | 2 +- .../items/category/[category]/+server.ts | 2 +- src/routes/api/[recipeLang=recipeLang]/items/icon/+server.ts | 3 +-- .../api/[recipeLang=recipeLang]/items/icon/[icon]/+server.ts | 2 +- .../items/in_season/[month]/+server.ts | 2 +- src/routes/api/[recipeLang=recipeLang]/items/tag/+server.ts | 4 ++-- .../api/[recipeLang=recipeLang]/items/tag/[tag]/+server.ts | 2 +- src/routes/api/[recipeLang=recipeLang]/offline-db/+server.ts | 4 ++-- src/routes/api/[recipeLang=recipeLang]/search/+server.ts | 2 +- .../[recipeLang=recipeLang]/translate/untranslated/+server.ts | 2 +- 14 files changed, 17 insertions(+), 18 deletions(-) diff --git a/TODO.md b/TODO.md index 92ed5c60..63ee662f 100644 --- a/TODO.md +++ b/TODO.md @@ -6,7 +6,7 @@ Order = impact. Font items + app.html preload intentionally skipped. - [x] 1. Lucide subpath imports — convert `from '@lucide/svelte'` barrel imports to `@lucide/svelte/icons/` so Vite tree-shakes per-icon (current 748 KB shared chunk) - [x] 2. Chart.js dynamic import in `FitnessChart.svelte` (drop 244 KB from non-stats fitness routes) -- [ ] 3. Recipe `all_brief` endpoint — drop `JSON.parse(JSON.stringify(...))`, move shuffle client-side, enable caching +- [x] 3. Recipe API endpoints — drop `JSON.parse(JSON.stringify(...))` double-serialize (9 endpoints). Client-side shuffle / cache headers deferred (would require rethinking hero preload + hydration) - [ ] 4. Favorites page — drop unnecessary `all_brief` fetch (verify consumer first) - [ ] 5. Replace redundant `locals.auth()` with `locals.session` across recipe/calendar/fitness loaders - [ ] 6. Stream fitness stats loader — return promises for slow panels diff --git a/package.json b/package.json index e04f6c98..202ba593 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "homepage", - "version": "1.46.14", + "version": "1.46.15", "private": true, "type": "module", "scripts": { diff --git a/src/routes/api/[recipeLang=recipeLang]/favorites/recipes/+server.ts b/src/routes/api/[recipeLang=recipeLang]/favorites/recipes/+server.ts index 17ac74bf..911e91e7 100644 --- a/src/routes/api/[recipeLang=recipeLang]/favorites/recipes/+server.ts +++ b/src/routes/api/[recipeLang=recipeLang]/favorites/recipes/+server.ts @@ -65,10 +65,10 @@ export const GET: RequestHandler = async ({ params, locals }) => { germanShortName: recipe.short_name, translationStatus: t?.translationStatus }}); - return json(JSON.parse(JSON.stringify(englishRecipes))); + return json(englishRecipes); } - return json(JSON.parse(JSON.stringify(recipes))); + return json(recipes); } catch (e) { throw error(500, 'Failed to fetch favorite recipes'); } diff --git a/src/routes/api/[recipeLang=recipeLang]/items/all_brief/+server.ts b/src/routes/api/[recipeLang=recipeLang]/items/all_brief/+server.ts index 5566ca69..e31eff73 100644 --- a/src/routes/api/[recipeLang=recipeLang]/items/all_brief/+server.ts +++ b/src/routes/api/[recipeLang=recipeLang]/items/all_brief/+server.ts @@ -11,5 +11,5 @@ export const GET: RequestHandler = async ({ params }) => { const dbRecipes = await Recipe.find(approvalFilter, projection).lean(); const recipes = dbRecipes.map(r => toBrief(r, params.recipeLang!)); - return json(JSON.parse(JSON.stringify(rand_array(recipes)))); + return json(rand_array(recipes)); }; diff --git a/src/routes/api/[recipeLang=recipeLang]/items/category/+server.ts b/src/routes/api/[recipeLang=recipeLang]/items/category/+server.ts index e89d67a7..2a2a0256 100644 --- a/src/routes/api/[recipeLang=recipeLang]/items/category/+server.ts +++ b/src/routes/api/[recipeLang=recipeLang]/items/category/+server.ts @@ -10,5 +10,5 @@ export const GET: RequestHandler = async ({ params }) => { const field = `${prefix}category`; const categories = await Recipe.distinct(field, approvalFilter).lean(); - return json(JSON.parse(JSON.stringify(categories))); + return json(categories); }; diff --git a/src/routes/api/[recipeLang=recipeLang]/items/category/[category]/+server.ts b/src/routes/api/[recipeLang=recipeLang]/items/category/[category]/+server.ts index 85efc870..fa2a48dc 100644 --- a/src/routes/api/[recipeLang=recipeLang]/items/category/[category]/+server.ts +++ b/src/routes/api/[recipeLang=recipeLang]/items/category/[category]/+server.ts @@ -14,5 +14,5 @@ export const GET: RequestHandler = async ({ params }) => { ).lean(); const recipes = rand_array(dbRecipes.map(r => toBrief(r, params.recipeLang!))); - return json(JSON.parse(JSON.stringify(recipes))); + return json(recipes); }; diff --git a/src/routes/api/[recipeLang=recipeLang]/items/icon/+server.ts b/src/routes/api/[recipeLang=recipeLang]/items/icon/+server.ts index 96f7e07f..62f4c686 100644 --- a/src/routes/api/[recipeLang=recipeLang]/items/icon/+server.ts +++ b/src/routes/api/[recipeLang=recipeLang]/items/icon/+server.ts @@ -5,8 +5,7 @@ import type {BriefRecipeType} from '$types/types'; export const GET: RequestHandler = async ({params}) => { await dbConnect(); - let icons = (await Recipe.distinct('icon').lean()); + const icons = await Recipe.distinct('icon').lean(); - icons = JSON.parse(JSON.stringify(icons)); return json(icons); }; diff --git a/src/routes/api/[recipeLang=recipeLang]/items/icon/[icon]/+server.ts b/src/routes/api/[recipeLang=recipeLang]/items/icon/[icon]/+server.ts index ae00f020..f74a311a 100644 --- a/src/routes/api/[recipeLang=recipeLang]/items/icon/[icon]/+server.ts +++ b/src/routes/api/[recipeLang=recipeLang]/items/icon/[icon]/+server.ts @@ -14,5 +14,5 @@ export const GET: RequestHandler = async ({ params }) => { ).lean(); const recipes = rand_array(dbRecipes.map(r => toBrief(r, params.recipeLang!))); - return json(JSON.parse(JSON.stringify(recipes))); + return json(recipes); }; diff --git a/src/routes/api/[recipeLang=recipeLang]/items/in_season/[month]/+server.ts b/src/routes/api/[recipeLang=recipeLang]/items/in_season/[month]/+server.ts index 1944299b..04681ddc 100644 --- a/src/routes/api/[recipeLang=recipeLang]/items/in_season/[month]/+server.ts +++ b/src/routes/api/[recipeLang=recipeLang]/items/in_season/[month]/+server.ts @@ -14,5 +14,5 @@ export const GET: RequestHandler = async ({ params }) => { ).lean(); const recipes = dbRecipes.map(r => toBrief(r, params.recipeLang!)); - return json(JSON.parse(JSON.stringify(rand_array(recipes)))); + return json(rand_array(recipes)); }; diff --git a/src/routes/api/[recipeLang=recipeLang]/items/tag/+server.ts b/src/routes/api/[recipeLang=recipeLang]/items/tag/+server.ts index 74ef7607..c0066f15 100644 --- a/src/routes/api/[recipeLang=recipeLang]/items/tag/+server.ts +++ b/src/routes/api/[recipeLang=recipeLang]/items/tag/+server.ts @@ -15,9 +15,9 @@ export const GET: RequestHandler = async ({ params }) => { recipe.translations.en.tags.forEach((tag: string) => tagsSet.add(tag)); } }); - return json(JSON.parse(JSON.stringify(Array.from(tagsSet).sort()))); + return json(Array.from(tagsSet).sort()); } const tags = await Recipe.distinct('tags').lean(); - return json(JSON.parse(JSON.stringify(tags))); + return json(tags); }; diff --git a/src/routes/api/[recipeLang=recipeLang]/items/tag/[tag]/+server.ts b/src/routes/api/[recipeLang=recipeLang]/items/tag/[tag]/+server.ts index 802dbd1b..ccb6cc8d 100644 --- a/src/routes/api/[recipeLang=recipeLang]/items/tag/[tag]/+server.ts +++ b/src/routes/api/[recipeLang=recipeLang]/items/tag/[tag]/+server.ts @@ -14,5 +14,5 @@ export const GET: RequestHandler = async ({ params }) => { ).lean(); const recipes = dbRecipes.map(r => toBrief(r, params.recipeLang!)); - return json(JSON.parse(JSON.stringify(rand_array(recipes)))); + return json(rand_array(recipes)); }; diff --git a/src/routes/api/[recipeLang=recipeLang]/offline-db/+server.ts b/src/routes/api/[recipeLang=recipeLang]/offline-db/+server.ts index 60a3e7b8..71718546 100644 --- a/src/routes/api/[recipeLang=recipeLang]/offline-db/+server.ts +++ b/src/routes/api/[recipeLang=recipeLang]/offline-db/+server.ts @@ -72,8 +72,8 @@ export const GET: RequestHandler = async () => { }); return json({ - brief: JSON.parse(JSON.stringify(briefRecipes)), - full: JSON.parse(JSON.stringify(processedFullRecipes)), + brief: briefRecipes, + full: processedFullRecipes, syncedAt: new Date().toISOString() }); }; diff --git a/src/routes/api/[recipeLang=recipeLang]/search/+server.ts b/src/routes/api/[recipeLang=recipeLang]/search/+server.ts index 9a3d40d4..0e3f71fe 100644 --- a/src/routes/api/[recipeLang=recipeLang]/search/+server.ts +++ b/src/routes/api/[recipeLang=recipeLang]/search/+server.ts @@ -71,7 +71,7 @@ export const GET: RequestHandler = async ({ url, params, locals }) => { }); } - return json(JSON.parse(JSON.stringify(recipes))); + return json(recipes); } catch (e) { return json({ error: 'Search failed' }, { status: 500 }); } diff --git a/src/routes/api/[recipeLang=recipeLang]/translate/untranslated/+server.ts b/src/routes/api/[recipeLang=recipeLang]/translate/untranslated/+server.ts index 54244fc0..668b0182 100644 --- a/src/routes/api/[recipeLang=recipeLang]/translate/untranslated/+server.ts +++ b/src/routes/api/[recipeLang=recipeLang]/translate/untranslated/+server.ts @@ -40,7 +40,7 @@ export const GET: RequestHandler = async ({ locals }) => { translationStatus: recipe.translations?.en?.translationStatus || undefined })); - return json(JSON.parse(JSON.stringify(result))); + return json(result); } catch (e) { console.error('Error fetching untranslated recipes:', e); throw error(500, 'Fehler beim Laden der unübersetzten Rezepte');