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
+2 -9
View File
@@ -33,16 +33,9 @@ import { About } from './about';
import { UseStateProvider } from '../../components/UseStateProvider';
import { stopPropagation } from '../../utils/keyboard';
import { LogoutDialog } from '../../components/LogoutDialog';
import { SettingsPages } from './pages';
export enum SettingsPages {
GeneralPage,
AccountPage,
NotificationPage,
DevicesPage,
EmojisStickersPage,
DeveloperToolsPage,
AboutPage,
}
export { SettingsPages };
type SettingsMenuItem = {
page: SettingsPages;
@@ -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>
);
}
@@ -67,6 +67,7 @@ import {
makePresetId,
normalizeLabel,
} from '../../../utils/statusPresets';
import { STATUS_EXPIRY_KEY, STATUS_MSG_KEY } from '../../../utils/statusKeys';
type ProfileProps = {
profile: UserProfile;
@@ -365,8 +366,7 @@ function ProfileDisplayName({ profile, userId }: ProfileProps) {
);
}
export const STATUS_EXPIRY_KEY = (id: string) => `lotus-status-expiry-${id}`;
export const STATUS_MSG_KEY = (id: string) => `lotus-status-msg-${id}`;
export { STATUS_EXPIRY_KEY, STATUS_MSG_KEY };
const CLEAR_AFTER_OPTIONS = [
{ label: 'Never', value: '0' },
+2 -1
View File
@@ -1 +1,2 @@
export * from './Settings';
export { SettingsPages } from './pages';
export { Settings } from './SettingsLazy';
+11
View File
@@ -0,0 +1,11 @@
/** Settings dialog pages. Kept apart from Settings.tsx so callers can name a
* page without pulling the dialog into the entry chunk. */
export enum SettingsPages {
GeneralPage,
AccountPage,
NotificationPage,
DevicesPage,
EmojisStickersPage,
DeveloperToolsPage,
AboutPage,
}
+1 -1
View File
@@ -63,7 +63,7 @@ import {
migrateLocalMuteTimers,
unmuteRoom,
} from '../../features/room-nav/muteTimers';
import { STATUS_EXPIRY_KEY, STATUS_MSG_KEY } from '../../features/settings/account/Profile';
import { STATUS_EXPIRY_KEY, STATUS_MSG_KEY } from '../../utils/statusKeys';
import { noteLocalStatusWrite } from '../../utils/ownStatusSync';
import { setPresenceWithRetry } from '../../utils/presenceWrite';
import { useDeepLinkNavigate } from '../../hooks/useDeepLinkNavigate';
+3
View File
@@ -0,0 +1,3 @@
/** localStorage keys for this device's copy of the status message and its expiry. */
export const STATUS_EXPIRY_KEY = (id: string) => `lotus-status-expiry-${id}`;
export const STATUS_MSG_KEY = (id: string) => `lotus-status-msg-${id}`;
+19
View File
@@ -251,6 +251,25 @@ const vendorChunks = (id) => {
if (id.includes('node_modules/immer')) return 'immer';
if (id.includes('node_modules/folds')) return 'folds';
if (id.includes('node_modules/emojibase')) return 'emojibase';
// HTML sanitising (sanitize-html pulls in postcss + htmlparser2) and colour
// maths are needed at startup but are big, stable libraries: keep them in
// their own cacheable chunks instead of wherever the chunk graph drops them
// (once Settings became lazy they landed in the entry chunk).
if (
id.includes('node_modules/sanitize-html') ||
id.includes('node_modules/postcss') ||
id.includes('node_modules/htmlparser2') ||
id.includes('node_modules/domhandler') ||
id.includes('node_modules/domutils') ||
id.includes('node_modules/dom-serializer') ||
id.includes('node_modules/entities') ||
id.includes('node_modules/parse-srcset') ||
id.includes('node_modules/nanoid') ||
id.includes('node_modules/picocolors') ||
id.includes('node_modules/source-map-js')
)
return 'sanitize-html';
if (id.includes('node_modules/chroma-js')) return 'chroma';
};
export default defineConfig({