perf: lazy-load seasonal overlays as their own chunks (#167)
CI / Build & Quality Checks (push) Canceled after 0s
CI / Trigger Desktop Build (push) Canceled after 0s
CI / Secret scan (gitleaks) (push) Canceled after 0s
CI / Docker image build & smoke test (push) Canceled after 0s
CI / Playwright smoke (e2e) (push) Canceled after 0s

All 11 seasonal overlays (particles + vanilla-extract keyframes) were in the
main JS and CSS for everyone year-round. Each is now a React.lazy chunk fetched
only when that season is active (auto date or override) or previewed in
Settings → Appearance; the schedule/override decision stays in the main bundle.

check-bundle-size: largest chunk 332.4 kB → 320.1 kB gzip; main CSS 68.4 kB →
56.5 kB; total gzip 1551.6 kB → 1561.3 kB (per-chunk overhead, only paid when
a season is on). Verified headless: no theme chunk requested at startup with
'auto' out of season; the Settings grid fetches the 11 previews; picking
Halloween renders the full-screen overlay (20 particle nodes).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PPmy3tPq869XDW4njjVaKA
This commit is contained in:
2026-09-19 12:59:28 -04:00
co-authored by Claude Opus 5
parent 2e7915d086
commit 8658ec05c3
+42 -14
View File
@@ -1,21 +1,49 @@
import React, { useEffect, useMemo, useState } from 'react';
import React, { Suspense, useEffect, useMemo, useState } from 'react';
import { useAtomValue } from 'jotai';
import { settingsAtom } from '../../state/settings';
import { useReducedMotion } from '../../hooks/useReducedMotion';
import { zIndices } from '../../styles/zIndex';
import { SeasonTheme } from './types';
import { resolveSeasonTheme } from './seasonSchedule';
import { HalloweenOverlay } from './themes/Halloween';
import { ChristmasOverlay } from './themes/Christmas';
import { NewYearOverlay } from './themes/NewYear';
import { AutumnOverlay } from './themes/Autumn';
import { AprilFoolsOverlay } from './themes/AprilFools';
import { LunarNewYearOverlay } from './themes/LunarNewYear';
import { ValentinesOverlay } from './themes/Valentines';
import { StPatricksOverlay } from './themes/StPatricks';
import { EarthDayOverlay } from './themes/EarthDay';
import { DeepSpaceOverlay } from './themes/DeepSpace';
import { ArcadeOverlay } from './themes/Arcade';
// [Gitea #167] Each overlay (its particles + keyframes) is its own chunk,
// fetched only when that season is active or previewed in Settings — the
// date/override decision stays in the main bundle.
const lazyOverlay = (load: () => Promise<{ default: React.ComponentType<{ reduced: boolean }> }>) =>
React.lazy(load);
const HalloweenOverlay = lazyOverlay(() =>
import('./themes/Halloween').then((m) => ({ default: m.HalloweenOverlay })),
);
const ChristmasOverlay = lazyOverlay(() =>
import('./themes/Christmas').then((m) => ({ default: m.ChristmasOverlay })),
);
const NewYearOverlay = lazyOverlay(() =>
import('./themes/NewYear').then((m) => ({ default: m.NewYearOverlay })),
);
const AutumnOverlay = lazyOverlay(() =>
import('./themes/Autumn').then((m) => ({ default: m.AutumnOverlay })),
);
const AprilFoolsOverlay = lazyOverlay(() =>
import('./themes/AprilFools').then((m) => ({ default: m.AprilFoolsOverlay })),
);
const LunarNewYearOverlay = lazyOverlay(() =>
import('./themes/LunarNewYear').then((m) => ({ default: m.LunarNewYearOverlay })),
);
const ValentinesOverlay = lazyOverlay(() =>
import('./themes/Valentines').then((m) => ({ default: m.ValentinesOverlay })),
);
const StPatricksOverlay = lazyOverlay(() =>
import('./themes/StPatricks').then((m) => ({ default: m.StPatricksOverlay })),
);
const EarthDayOverlay = lazyOverlay(() =>
import('./themes/EarthDay').then((m) => ({ default: m.EarthDayOverlay })),
);
const DeepSpaceOverlay = lazyOverlay(() =>
import('./themes/DeepSpace').then((m) => ({ default: m.DeepSpaceOverlay })),
);
const ArcadeOverlay = lazyOverlay(() =>
import('./themes/Arcade').then((m) => ({ default: m.ArcadeOverlay })),
);
// SeasonTheme + the date-window logic now live in leaf modules (single source
// of truth, shared with the settings UI). Re-exported here for existing
@@ -69,7 +97,7 @@ function SeasonalOverlay({ theme, reduced }: { theme: SeasonTheme; reduced: bool
overflow: 'hidden',
}}
>
{buildOverlayContent(theme, reduced)}
<Suspense fallback={null}>{buildOverlayContent(theme, reduced)}</Suspense>
</div>
);
}
@@ -94,7 +122,7 @@ export function SeasonalPreview({ theme }: { theme: SeasonTheme }) {
containerType: 'inline-size',
}}
>
{buildOverlayContent(theme, true)}
<Suspense fallback={null}>{buildOverlayContent(theme, true)}</Suspense>
</div>
);
}