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.
This commit is contained in:
Claude
2026-06-19 22:56:00 +00:00
parent b54c9a3e71
commit c60506cc36
8 changed files with 160 additions and 23 deletions
+12 -2
View File
@@ -17,7 +17,7 @@
import { fileDownloadUrl, renameFile, deleteFile } from '$lib/api/endpoints/files';
import { renameFolder, deleteFolder } from '$lib/api/endpoints/folders';
import type { FileItem, ItemType } from '$lib/api/types';
import FileViewer from '$lib/components/FileViewer.svelte';
import { lazyComponent } from '$lib/composables/lazyComponent.svelte';
import MoveDialog from '$lib/components/MoveDialog.svelte';
import ShareDialog from '$lib/components/ShareDialog.svelte';
import ResourceList, {
@@ -134,6 +134,13 @@
let viewerOpen = $state(false);
let viewerFile = $state<FileItem | null>(null);
// The file preview is loaded the first time a file is opened, keeping its
// module out of this route's initial chunk.
const fileViewer = lazyComponent(() => import('$lib/components/FileViewer.svelte'));
$effect(() => {
if (viewerOpen) void fileViewer.load();
});
function open(entry: ResourceEntry) {
if (entry.kind === 'folder') {
goto(`/files/${entry.id}`);
@@ -349,7 +356,10 @@
{/snippet}
</ResourceList>
<FileViewer bind:open={viewerOpen} file={viewerFile} />
{#if fileViewer.component}
{@const FileViewer = fileViewer.component}
<FileViewer bind:open={viewerOpen} file={viewerFile} />
{/if}
<MoveDialog
bind:open={moveOpen}
item={moveTarget}