From 88f49e98ff61cd8ef357878ab90f4a307700adfd Mon Sep 17 00:00:00 2001 From: "Michael H." Date: Wed, 5 Aug 2026 20:30:12 +0200 Subject: [PATCH 1/3] feat: server source maps --- apps/frontend/wrangler.jsonc | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/frontend/wrangler.jsonc b/apps/frontend/wrangler.jsonc index 821c5c3e9a..22da20148b 100644 --- a/apps/frontend/wrangler.jsonc +++ b/apps/frontend/wrangler.jsonc @@ -10,6 +10,7 @@ "routes": ["modrinth.com/*"], "preview_urls": true, "workers_dev": true, + "upload_source_maps": true, "limits": { "cpu_ms": 5000 }, From e38b9f478f4447aefe938b4ee443419da3f6493a Mon Sep 17 00:00:00 2001 From: "Michael H." Date: Wed, 5 Aug 2026 20:46:21 +0200 Subject: [PATCH 2/3] perf: strip unused fields from search results --- .../src/pages/discover/[type]/index.vue | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/apps/frontend/src/pages/discover/[type]/index.vue b/apps/frontend/src/pages/discover/[type]/index.vue index 8b24b6f5f2..993c13eabe 100644 --- a/apps/frontend/src/pages/discover/[type]/index.vue +++ b/apps/frontend/src/pages/discover/[type]/index.vue @@ -228,6 +228,19 @@ function parseSearchParams(requestParams: string): Labrinth.Search.SearchParams } } +// Search returns expanded dependency data that no card renders and that isn't part of +// ResultSearchProject. On modpacks it is ~90% of the response, and everything cached here +// is serialized into the SSR payload, so drop it before it reaches the query cache. +function stripUnrenderedFields( + hits: Labrinth.Search.v3.ResultSearchProject[], +): Labrinth.Search.v3.ResultSearchProject[] { + return hits.map((hit) => { + const { dependencies, dependency_project_ids, compatible_dependency_project_ids, ...rendered } = + hit as Labrinth.Search.v3.ResultSearchProject & Record + return rendered as Labrinth.Search.v3.ResultSearchProject + }) +} + async function fetchSearch(requestParams: string) { debug('search() called', { requestParams: requestParams.substring(0, 100), @@ -241,17 +254,19 @@ async function fetchSearch(requestParams: string) { debug('search() response', { total_hits: raw.total_hits, hitCount: raw.hits?.length }) + const hits = stripUnrenderedFields(raw.hits ?? []) + if (isServerType.value) { return { projectHits: [], - serverHits: raw.hits, + serverHits: hits, total_hits: raw.total_hits, per_page: raw.hits_per_page, } } return { - projectHits: raw.hits, + projectHits: hits, serverHits: [], total_hits: raw.total_hits, per_page: raw.hits_per_page, From 5e557a95897b4ea450a0385d6a1420f5c5cf1be5 Mon Sep 17 00:00:00 2001 From: "Michael H." Date: Wed, 5 Aug 2026 20:47:19 +0200 Subject: [PATCH 3/3] perf: reduce game version memory usage --- apps/frontend/src/composables/generated.ts | 35 ++++++++++++++++--- .../plugins/update-game-versions.client.ts | 19 ++++++++++ .../src/plugins/update-game-versions.ts | 17 --------- 3 files changed, 49 insertions(+), 22 deletions(-) create mode 100644 apps/frontend/src/plugins/update-game-versions.client.ts delete mode 100644 apps/frontend/src/plugins/update-game-versions.ts diff --git a/apps/frontend/src/composables/generated.ts b/apps/frontend/src/composables/generated.ts index e178ab048a..8a7b895e0f 100644 --- a/apps/frontend/src/composables/generated.ts +++ b/apps/frontend/src/composables/generated.ts @@ -68,11 +68,16 @@ export interface GeneratedState extends GloballyUsedState { } /** - * Composable for accessing the globally used generated state. - * This includes both fetched data and runtime-defined constants. + * Built once per module load rather than via `useState`, because every `useState` value is + * serialized into the SSR payload of every page. This is build-time constant data that the + * client already has via the static import above, so putting it in the payload would ship a + * second copy for no benefit. + * + * Consequence: on the server this object is shared by every request in the isolate, so it must + * stay immutable there. Runtime refreshes are client-only and go through `setGameVersions`. */ -export const useGeneratedState = () => - useState('generatedState', () => ({ +const generatedState = shallowRef( + Object.freeze({ // Cast JSON data to typed API responses categories: (categories ?? []) as Labrinth.Tags.v2.Category[], loaders: (loaders ?? []) as Labrinth.Tags.v2.Loader[], @@ -147,4 +152,24 @@ export const useGeneratedState = () => errors, buildYear: new Date().getFullYear(), - })) + }) as GeneratedState, +) + +/** + * Composable for accessing the globally used generated state. + * This includes both fetched data and runtime-defined constants. + */ +export const useGeneratedState = () => generatedState + +/** + * Replaces the build-time game versions with a freshly fetched list. Client-only: mutating this + * on the server would leak across every request sharing the isolate. + */ +export function setGameVersions(versions: Labrinth.Tags.v2.GameVersion[]) { + if (import.meta.server) return + + generatedState.value = Object.freeze({ + ...generatedState.value, + gameVersions: versions, + }) as GeneratedState +} diff --git a/apps/frontend/src/plugins/update-game-versions.client.ts b/apps/frontend/src/plugins/update-game-versions.client.ts new file mode 100644 index 0000000000..b73976b254 --- /dev/null +++ b/apps/frontend/src/plugins/update-game-versions.client.ts @@ -0,0 +1,19 @@ +import type { Labrinth } from '@modrinth/api-client' + +import { setGameVersions } from '~/composables/generated' + +export default defineNuxtPlugin((nuxtApp) => { + // Deferred until after hydration: the server renders with the build-time game versions, so + // swapping them in during setup would make the first client render disagree with the markup. + nuxtApp.hook('app:suspense:resolve', async () => { + try { + const gameVersions = await $fetch('/api/tags/game-versions') + + if (gameVersions && gameVersions.length > 0) { + setGameVersions(gameVersions) + } + } catch (error) { + console.error('[Game Version Updater] Failed to fetch:', error) + } + }) +}) diff --git a/apps/frontend/src/plugins/update-game-versions.ts b/apps/frontend/src/plugins/update-game-versions.ts deleted file mode 100644 index 5ded43620b..0000000000 --- a/apps/frontend/src/plugins/update-game-versions.ts +++ /dev/null @@ -1,17 +0,0 @@ -import type { Labrinth } from '@modrinth/api-client' - -export default defineNuxtPlugin(async () => { - try { - const gameVersions = await $fetch('/api/tags/game-versions') - - if (gameVersions && gameVersions.length > 0) { - const state = useState<{ gameVersions: Labrinth.Tags.v2.GameVersion[] }>('generatedState') - - if (state.value) { - state.value.gameVersions = gameVersions - } - } - } catch (error) { - console.error('[Game Version Updater] Failed to fetch:', error) - } -})