perf: load the Settings dialog on demand; entry chunk 351.8 → 277.1 kB gzip
CI / Build & Quality Checks (pull_request) Canceled after 0s
CI / Trigger Desktop Build (pull_request) Canceled after 0s
CI / Secret scan (gitleaks) (pull_request) Canceled after 0s
CI / Docker image build & smoke test (pull_request) Canceled after 0s
CI / Playwright smoke (e2e) (pull_request) Canceled after 0s

Settings (every page, pickers, the denoise tester) was in the entry
chunk though it only renders when opened. Load it lazily behind a
focusable spinner (it opens inside a FocusTrap), and warm the chunk
when the app goes idle. SettingsPages and the status localStorage keys
move to tiny modules so callers don't pull the dialog back in.
sanitize-html (+postcss, htmlparser2) and chroma-js get their own vendor
chunks; with Settings gone the chunk graph had dropped them into the
entry chunk.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017PCb59uY3UKqDqt7B6hzd5
This commit is contained in:
Lotus CI
2026-10-19 00:00:19 -04:00
co-authored by Claude Opus 5.5
parent d7bc6d94bb
commit 8b6464c8b9
8 changed files with 83 additions and 13 deletions
@@ -0,0 +1,43 @@
import React, { lazy, Suspense } from 'react';
import { Box, Spinner, config } from 'folds';
import type { SettingsPages } from './pages';
// Settings is a large, rarely-open dialog (all its pages, pickers, the
// denoise tester…). Load it on first open instead of in the entry chunk, and
// warm the chunk once the app is idle so opening it rarely waits.
const loadSettings = () => import('./Settings');
const SettingsDialog = lazy(() => loadSettings().then((m) => ({ default: m.Settings })));
if (typeof window !== 'undefined') {
const warm = () => {
loadSettings().catch(() => undefined);
};
if ('requestIdleCallback' in window) window.requestIdleCallback(warm, { timeout: 10_000 });
else setTimeout(warm, 5_000);
}
type SettingsProps = {
initialPage?: SettingsPages;
requestClose: () => void;
};
export function Settings(props: SettingsProps) {
return (
<Suspense
fallback={
// Settings opens inside a FocusTrap, which needs a tabbable node.
<Box
tabIndex={0}
role="status"
aria-label="Loading settings"
alignItems="Center"
justifyContent="Center"
style={{ padding: config.space.S700, outline: 'none' }}
>
<Spinner variant="Secondary" size="400" />
</Box>
}
>
<SettingsDialog {...props} />
</Suspense>
);
}