From c60506cc36b66f77997e0c3a1abcbb15ab583786 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 19 Jun 2026 22:56:00 +0000 Subject: [PATCH 1/2] perf(frontend): lazy-load heavy components to shrink initial bundle Defer loading components that are off the initial render path until they are first needed, keeping them out of the bundle that loads on page entry. - Add `lazyComponent` composable: a tiny rune-based holder that dynamic- imports a component on first `load()` and exposes it for `{@const}` rendering, with the component type inferred from the module so prop/ binding type-checking stays intact. - CommandPalette: loaded on the first Cmd/Ctrl+K and mounted open via a new `autoOpen` prop. It was previously imported by AppShell, i.e. in the initial chunk of every authenticated route (~27 KB). AppShell now owns the shortcut that triggers the load. - FileViewer + WopiEditor: loaded when a preview/editor first opens, across files, recent, favorites and shared-with-me. - PhotoLightbox / PlacesMap / PeopleView: loaded on first lightbox open or when the places/people tab is selected (the latter also defers maplibre). Once loaded a component stays mounted and behaves exactly as before, so behavior is unchanged on the second use onward. --- frontend/src/lib/components/AppShell.svelte | 18 ++++++++- .../src/lib/components/CommandPalette.svelte | 16 ++++++++ .../lib/composables/lazyComponent.svelte.ts | 39 +++++++++++++++++++ frontend/src/routes/favorites/+page.svelte | 14 ++++++- .../src/routes/files/[...path]/+page.svelte | 36 ++++++++++++----- frontend/src/routes/photos/+page.svelte | 32 ++++++++++++--- frontend/src/routes/recent/+page.svelte | 14 ++++++- .../src/routes/shared-with-me/+page.svelte | 14 ++++++- 8 files changed, 160 insertions(+), 23 deletions(-) create mode 100644 frontend/src/lib/composables/lazyComponent.svelte.ts diff --git a/frontend/src/lib/components/AppShell.svelte b/frontend/src/lib/components/AppShell.svelte index f291aa17..de5b2d8b 100644 --- a/frontend/src/lib/components/AppShell.svelte +++ b/frontend/src/lib/components/AppShell.svelte @@ -6,7 +6,7 @@ import { searchFiles } from '$lib/api/endpoints/search'; import { fileInlineUrl } from '$lib/api/endpoints/files'; import type { FileItem, FolderItem } from '$lib/api/types'; - import CommandPalette from '$lib/components/CommandPalette.svelte'; + import { lazyComponent } from '$lib/composables/lazyComponent.svelte'; import Icon from '$lib/icons/Icon.svelte'; import { iconNameFromClass } from '$lib/utils/display'; import { userInitials, avatarColorIndex } from '$lib/utils/avatar'; @@ -19,6 +19,10 @@ let { children }: { children: Snippet } = $props(); + // The command palette is loaded on its first Cmd/Ctrl+K and mounted open. + // Until then its ~400-line module stays out of the initial bundle. + const palette = lazyComponent(() => import('$lib/components/CommandPalette.svelte')); + interface NavLink { href: string; label: string; @@ -224,6 +228,13 @@ { + // First Cmd/Ctrl+K loads the palette and mounts it open; once mounted, + // the palette's own handler takes over toggling/closing. + if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === 'k' && !palette.component) { + e.preventDefault(); + void palette.load(); + return; + } if (e.key !== 'Escape') return; if (aboutOpen) aboutOpen = false; else if (searchActive) closeMobileSearch(); @@ -683,7 +694,10 @@ {/if} - +{#if palette.component} + {@const CommandPalette = palette.component} + +{/if}