Merge w11/perf into release/v1.18.11

Serve achievements via SWR and precompute Berlin day-keys to kill the per-row
Intl cost; switch dashboard/summary to cachedSwr; lazy-load three charts that
bypassed the dynamic boundary.
This commit is contained in:
Marc-André Bombeck committed 2026-06-20 19:05:27 +02:00
commit 6975849bec
10 files changed
+357 -35

No files matched your search

+14 -3
View File
@@ -55,7 +55,7 @@ import { CUMULATIVE_HK_TYPES } from "@/lib/measurements/apple-health-mapping";
import { getServerTranslator } from "@/lib/i18n/server-translator";
import { defaultLocale, locales, type Locale } from "@/lib/i18n/config";
import { userDayKey, DEFAULT_TIMEZONE } from "@/lib/tz/resolver";
import { cached, caches, type ServerCache } from "@/lib/cache/server-cache";
import { cachedSwr, caches, type ServerCache } from "@/lib/cache/server-cache";
import { buildMedsTodayBlock } from "@/lib/dashboard/meds-today";
import {
summarizeSleepNights,
@@ -328,12 +328,23 @@ export const GET = apiHandler(async () => {
// it).
const userTz = user.timezone ?? DEFAULT_TIMEZONE;
// v1.4.38 W-F — wrap the whole response in the 60 s analytics LRU.
// v1.4.38 W-F — wrap the whole response in the analytics LRU.
// Subsequent iOS polls inside the TTL hit memory; measurement /
// mood / medication writes invalidate the user-bucket via the
// existing `invalidateUserMeasurements` + the v1.4.38-extended
// `invalidateUserMedications` hooks.
const body = await cached(
//
// v1.18.11 (W5 perf) — read via `cachedSwr`. The `analytics` bucket
// already provisions a 1 h `staleTtlMs` window, but this endpoint read
// through plain `cached` and so ignored it: the iOS client polls every
// 60 s, and a plain read past the 60 s fresh TTL re-paid the full cold
// rebuild on every poll. `cachedSwr` serves the prior body instantly
// inside the stale window and warms one background recompute instead.
// Writes still mark/evict the bucket, so a user's own action is
// reflected on the very next read — the window only bounds wall-clock
// drift (the same self-correcting `nextDueOverdue`/tally drift the
// snapshot read already tolerates).
const body = await cachedSwr(
caches.analytics as ServerCache<
Awaited<ReturnType<typeof buildDashboardSummary>>
>,
@@ -3,7 +3,7 @@ import { apiHandler, requireAuth } from "@/lib/api-handler";
import { requireModuleEnabled, resolveModuleMap } from "@/lib/modules/gate";
import { annotate } from "@/lib/logging/context";
import { apiSuccess } from "@/lib/api-response";
import { cached, caches, type ServerCache } from "@/lib/cache/server-cache";
import { cachedSwr, caches, type ServerCache } from "@/lib/cache/server-cache";
import {
buildAchievementsResult,
type AchievementsResult,
@@ -72,7 +72,13 @@ export const GET = apiHandler(async (request: NextRequest) => {
// a toggle change is reflected on the next read.
const moduleMap = await resolveModuleMap(user.id);
const result = await cached(
// v1.18.11 (W5 perf) — read via `cachedSwr`. The bucket carries a
// 10-minute stale window; the app-wide `AchievementUnlockNotifier` polls
// every 2 minutes, so a hard-TTL read always missed and re-paid the cold
// build. SWR serves the prior payload instantly and warms one background
// recompute. Persistence of `pendingUnlocks` stays OUTSIDE this read (see
// below), so a stale-served body never skips an unlock write.
const result = await cachedSwr(
caches.achievements as ServerCache<AchievementsResult>,
user.id,
() => buildAchievementsResult(user, moduleMap),