mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-15 03:41:10 +02:00
perf(web): idle-defer non-critical (app) init + lazy-load modals
Homepage/workbench TTI was dominated by the layout synchronously booting event-bridge, streak tracker, LLM queue, memoro watcher, dashboard store, shared-uload and reminder scheduler before first paint, plus statically importing 7 modals/toasts/banners that are rarely-to-never visible on initial render. - Keep critical path inline: local-store init (manaStore/tag/link), unified sync engine + billing, guest-mode setup. - Move side-effect streams, projection workers and telemetry to requestIdleCallback (with setTimeout fallback). - Dynamically import KeyboardShortcutsModal, OnboardingWizard, GuestWelcomeModal, SessionWarning, EncryptionIntroBanner, SuggestionToast, NudgeToast. Modals fetch on first demand; toasts mount after idle so their transitive deps (automationsStore, day-snapshot projection, streaks, crypto gate) don't land in the initial chunk. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
c357a1cd1d
commit
b196e7782e
1 changed files with 151 additions and 60 deletions
|
|
@ -1,7 +1,7 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { goto } from '$app/navigation';
|
import { goto } from '$app/navigation';
|
||||||
import { page } from '$app/stores';
|
import { page } from '$app/stores';
|
||||||
import type { Snippet } from 'svelte';
|
import type { Component, Snippet } from 'svelte';
|
||||||
import { onDestroy, setContext } from 'svelte';
|
import { onDestroy, setContext } from 'svelte';
|
||||||
import { createReminderScheduler } from '@mana/shared-stores';
|
import { createReminderScheduler } from '@mana/shared-stores';
|
||||||
import { todoReminderSource } from '$lib/modules/todo/reminder-source';
|
import { todoReminderSource } from '$lib/modules/todo/reminder-source';
|
||||||
|
|
@ -9,12 +9,7 @@
|
||||||
import { initTools } from '$lib/data/tools/init';
|
import { initTools } from '$lib/data/tools/init';
|
||||||
import { startEventBridge, stopEventBridge } from '$lib/triggers/event-bridge';
|
import { startEventBridge, stopEventBridge } from '$lib/triggers/event-bridge';
|
||||||
import { startStreakTracker, stopStreakTracker } from '$lib/data/projections/streaks';
|
import { startStreakTracker, stopStreakTracker } from '$lib/data/projections/streaks';
|
||||||
import KeyboardShortcutsModal from '$lib/components/KeyboardShortcutsModal.svelte';
|
|
||||||
import SessionWarning from '$lib/components/SessionWarning.svelte';
|
|
||||||
import EncryptionIntroBanner from '$lib/components/EncryptionIntroBanner.svelte';
|
|
||||||
import { bottomBarStore } from '$lib/stores/bottom-bar.svelte';
|
import { bottomBarStore } from '$lib/stores/bottom-bar.svelte';
|
||||||
import SuggestionToast from '$lib/components/SuggestionToast.svelte';
|
|
||||||
import NudgeToast from '$lib/components/NudgeToast.svelte';
|
|
||||||
import { locale, _ } from 'svelte-i18n';
|
import { locale, _ } from 'svelte-i18n';
|
||||||
import {
|
import {
|
||||||
PillNavigation,
|
PillNavigation,
|
||||||
|
|
@ -37,7 +32,7 @@
|
||||||
import type { InputBarAdapter } from '$lib/quick-input/types';
|
import type { InputBarAdapter } from '$lib/quick-input/types';
|
||||||
import { getAdapterLoader } from '$lib/quick-input/registry';
|
import { getAdapterLoader } from '$lib/quick-input/registry';
|
||||||
import { createFallbackAdapter } from '$lib/quick-input/fallback-adapter';
|
import { createFallbackAdapter } from '$lib/quick-input/fallback-adapter';
|
||||||
import { AuthGate, GuestWelcomeModal } from '@mana/shared-auth-ui';
|
import { AuthGate } from '@mana/shared-auth-ui';
|
||||||
import { MANA_APPS, hasAppAccess, ACCESS_TIER_LABELS } from '@mana/shared-branding';
|
import { MANA_APPS, hasAppAccess, ACCESS_TIER_LABELS } from '@mana/shared-branding';
|
||||||
import type { AccessTier } from '@mana/shared-branding';
|
import type { AccessTier } from '@mana/shared-branding';
|
||||||
import { createGuestMode, type GuestMode } from '$lib/stores/guest-mode.svelte';
|
import { createGuestMode, type GuestMode } from '$lib/stores/guest-mode.svelte';
|
||||||
|
|
@ -78,7 +73,6 @@
|
||||||
import { isNavCollapsed as collapsedStore } from '$lib/stores/navigation';
|
import { isNavCollapsed as collapsedStore } from '$lib/stores/navigation';
|
||||||
import { getPillAppItems } from '@mana/shared-branding';
|
import { getPillAppItems } from '@mana/shared-branding';
|
||||||
import { onboardingStore } from '$lib/stores/onboarding.svelte';
|
import { onboardingStore } from '$lib/stores/onboarding.svelte';
|
||||||
import { OnboardingWizard } from '$lib/components/onboarding';
|
|
||||||
import { STORAGE_KEYS } from '$lib/config/storage-keys';
|
import { STORAGE_KEYS } from '$lib/config/storage-keys';
|
||||||
import { SearchRegistry } from '$lib/search/registry';
|
import { SearchRegistry } from '$lib/search/registry';
|
||||||
import { registerAllProviders } from '$lib/search/providers';
|
import { registerAllProviders } from '$lib/search/providers';
|
||||||
|
|
@ -89,6 +83,22 @@
|
||||||
|
|
||||||
let { children }: { children: Snippet } = $props();
|
let { children }: { children: Snippet } = $props();
|
||||||
|
|
||||||
|
// ── Idle-defer helper ───────────────────────────────────
|
||||||
|
// Runs work when the browser is idle so first interaction isn't
|
||||||
|
// blocked by non-critical init (telemetry, schedulers, side-effect
|
||||||
|
// streams). Falls back to setTimeout on browsers without
|
||||||
|
// requestIdleCallback.
|
||||||
|
function idle(cb: () => void, timeout = 2000) {
|
||||||
|
if (typeof window === 'undefined') return;
|
||||||
|
const ric = (
|
||||||
|
window as unknown as {
|
||||||
|
requestIdleCallback?: (cb: () => void, opts?: { timeout?: number }) => void;
|
||||||
|
}
|
||||||
|
).requestIdleCallback;
|
||||||
|
if (ric) ric(cb, { timeout });
|
||||||
|
else setTimeout(cb, 0);
|
||||||
|
}
|
||||||
|
|
||||||
// ── App switcher ────────────────────────────────────────
|
// ── App switcher ────────────────────────────────────────
|
||||||
let appItems = $derived(getPillAppItems('mana', undefined, undefined, authStore.user?.tier));
|
let appItems = $derived(getPillAppItems('mana', undefined, undefined, authStore.user?.tier));
|
||||||
|
|
||||||
|
|
@ -391,6 +401,63 @@
|
||||||
// ── Guest Mode ──────────────────────────────────────────
|
// ── Guest Mode ──────────────────────────────────────────
|
||||||
let guestMode = $state<GuestMode | null>(null);
|
let guestMode = $state<GuestMode | null>(null);
|
||||||
|
|
||||||
|
// ── Lazy-loaded UI (modals, toasts, banners) ────────────
|
||||||
|
// Static imports for these were adding weight to the initial layout
|
||||||
|
// bundle for components that are rarely-to-never visible on first
|
||||||
|
// paint. Each is fetched either on first demand (modals) or shortly
|
||||||
|
// after idle (always-mounted toasts/banners that self-gate).
|
||||||
|
// Permissive prop typing — props are validated at the call site
|
||||||
|
// where {@const} narrows the component back to its concrete type.
|
||||||
|
type AnyComponent = Component<any>;
|
||||||
|
let KeyboardShortcutsModalC = $state<AnyComponent | null>(null);
|
||||||
|
let OnboardingWizardC = $state<AnyComponent | null>(null);
|
||||||
|
let GuestWelcomeModalC = $state<AnyComponent | null>(null);
|
||||||
|
let SessionWarningC = $state<AnyComponent | null>(null);
|
||||||
|
let EncryptionIntroBannerC = $state<AnyComponent | null>(null);
|
||||||
|
let SuggestionToastC = $state<AnyComponent | null>(null);
|
||||||
|
let NudgeToastC = $state<AnyComponent | null>(null);
|
||||||
|
|
||||||
|
// On-demand: only fetch when the user actually opens them.
|
||||||
|
$effect(() => {
|
||||||
|
if (showShortcuts && !KeyboardShortcutsModalC) {
|
||||||
|
import('$lib/components/KeyboardShortcutsModal.svelte').then((m) => {
|
||||||
|
KeyboardShortcutsModalC = m.default;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
$effect(() => {
|
||||||
|
if (showOnboarding && !OnboardingWizardC) {
|
||||||
|
import('$lib/components/onboarding').then((m) => {
|
||||||
|
OnboardingWizardC = m.OnboardingWizard;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
$effect(() => {
|
||||||
|
if (guestMode?.showWelcome && !GuestWelcomeModalC) {
|
||||||
|
import('@mana/shared-auth-ui').then((m) => {
|
||||||
|
GuestWelcomeModalC = m.GuestWelcomeModal;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Idle-mount: background toasts/banners that self-gate internally.
|
||||||
|
// Deferring the import also defers their transitive deps
|
||||||
|
// (automationsStore, day-snapshot projection, streaks, crypto gate).
|
||||||
|
idle(() => {
|
||||||
|
void import('$lib/components/SuggestionToast.svelte').then((m) => {
|
||||||
|
SuggestionToastC = m.default;
|
||||||
|
});
|
||||||
|
void import('$lib/components/NudgeToast.svelte').then((m) => {
|
||||||
|
NudgeToastC = m.default;
|
||||||
|
});
|
||||||
|
void import('$lib/components/EncryptionIntroBanner.svelte').then((m) => {
|
||||||
|
EncryptionIntroBannerC = m.default;
|
||||||
|
});
|
||||||
|
void import('$lib/components/SessionWarning.svelte').then((m) => {
|
||||||
|
SessionWarningC = m.default;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
// ── Onboarding ──────────────────────────────────────────
|
// ── Onboarding ──────────────────────────────────────────
|
||||||
function handleOnboardingComplete() {
|
function handleOnboardingComplete() {
|
||||||
onboardingStore.complete();
|
onboardingStore.complete();
|
||||||
|
|
@ -415,32 +482,34 @@
|
||||||
setGuestPromptNavigator((href) => goto(href));
|
setGuestPromptNavigator((href) => goto(href));
|
||||||
if (authStore.isAuthenticated) guestPrompt.clear();
|
if (authStore.isAuthenticated) guestPrompt.clear();
|
||||||
|
|
||||||
// Phase A: Auth-independent — guests + authenticated
|
// Phase A (critical): the local-store inits are required before
|
||||||
|
// liveQueries anywhere downstream (TagStrip, module list views)
|
||||||
|
// can return non-empty results. Keep these awaited.
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
manaStore.initialize(),
|
manaStore.initialize(),
|
||||||
tagLocalStore.initialize(),
|
tagLocalStore.initialize(),
|
||||||
linkLocalStore.initialize(),
|
linkLocalStore.initialize(),
|
||||||
]);
|
]);
|
||||||
initSharedUload();
|
|
||||||
startEventStore();
|
|
||||||
initTools();
|
|
||||||
startEventBridge();
|
|
||||||
startStreakTracker();
|
|
||||||
await dashboardStore.initialize();
|
|
||||||
|
|
||||||
// Start the persistent LLM task queue. Idempotent — safe to call
|
// Phase A-idle: side-effect streams, telemetry, projection workers.
|
||||||
// repeatedly. The queue picks up any tasks left in 'pending' state
|
// All idempotent and self-gated; deferring to the next idle frame
|
||||||
// from previous sessions (and reclaims orphaned 'running' rows
|
// lets the first paint + interaction land without waiting on
|
||||||
// from a crashed session) before going idle. See $lib/llm-queue.ts.
|
// event-bridge wiring or LLM-queue reclaim work.
|
||||||
startLlmQueue();
|
idle(() => {
|
||||||
|
initSharedUload();
|
||||||
|
startEventStore();
|
||||||
|
initTools();
|
||||||
|
startEventBridge();
|
||||||
|
startStreakTracker();
|
||||||
|
startLlmQueue();
|
||||||
|
startMemoroLlmWatcher();
|
||||||
|
// dashboardStore only drives /dashboard — safe to defer; other
|
||||||
|
// routes don't read from it on first paint.
|
||||||
|
void dashboardStore.initialize();
|
||||||
|
reminderScheduler.start();
|
||||||
|
});
|
||||||
|
|
||||||
// Module-side LLM result watchers. Each subscribes via Dexie
|
// Restore nav collapsed state (cheap, keep inline)
|
||||||
// liveQuery to completed task rows tagged for its module and
|
|
||||||
// writes the results back to the module's own collection
|
|
||||||
// (e.g. memoro auto-titles → memo.title). Idempotent.
|
|
||||||
startMemoroLlmWatcher();
|
|
||||||
|
|
||||||
// Restore nav collapsed state
|
|
||||||
if (typeof localStorage !== 'undefined') {
|
if (typeof localStorage !== 'undefined') {
|
||||||
const savedCollapsed = localStorage.getItem(STORAGE_KEYS.NAV_COLLAPSED);
|
const savedCollapsed = localStorage.getItem(STORAGE_KEYS.NAV_COLLAPSED);
|
||||||
if (savedCollapsed === 'true') {
|
if (savedCollapsed === 'true') {
|
||||||
|
|
@ -449,10 +518,11 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Phase B: Auth-dependent — sync, settings, onboarding
|
// Phase B (critical): sync for authenticated users. Data delivery
|
||||||
|
// is user-visible via the pending-count badge, so we keep the
|
||||||
|
// sync engine boot on the critical path.
|
||||||
if (authStore.isAuthenticated) {
|
if (authStore.isAuthenticated) {
|
||||||
setErrorTrackingUser({ id: authStore.user?.id ?? 'unknown', email: authStore.user?.email });
|
setErrorTrackingUser({ id: authStore.user?.id ?? 'unknown', email: authStore.user?.email });
|
||||||
trackReturnVisit();
|
|
||||||
await syncBilling.load();
|
await syncBilling.load();
|
||||||
const getToken = () => authStore.getValidToken();
|
const getToken = () => authStore.getValidToken();
|
||||||
unifiedSync = createUnifiedSync(SYNC_SERVER_URL, getToken, syncBilling.active);
|
unifiedSync = createUnifiedSync(SYNC_SERVER_URL, getToken, syncBilling.active);
|
||||||
|
|
@ -486,18 +556,21 @@
|
||||||
// value (0 on a fresh tab) until a sync actually runs.
|
// value (0 on a fresh tab) until a sync actually runs.
|
||||||
refreshPendingCount();
|
refreshPendingCount();
|
||||||
|
|
||||||
userSettings.load().catch(() => {});
|
// Phase B-idle: settings, onboarding gating and return-visit
|
||||||
|
// telemetry. None of this gates rendering — onboarding shows
|
||||||
onboardingStore.load();
|
// via showOnboarding after the store resolves, which is fine
|
||||||
if (onboardingStore.shouldShow) {
|
// on a delay.
|
||||||
onboardingStore.start();
|
idle(async () => {
|
||||||
ManaEvents.onboardingStarted();
|
trackReturnVisit();
|
||||||
showOnboarding = true;
|
userSettings.load().catch(() => {});
|
||||||
}
|
await onboardingStore.load();
|
||||||
|
if (onboardingStore.shouldShow) {
|
||||||
|
onboardingStore.start();
|
||||||
|
ManaEvents.onboardingStarted();
|
||||||
|
showOnboarding = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Phase B2: Start reminder scheduler
|
|
||||||
reminderScheduler.start();
|
|
||||||
// IMPORTANT: do NOT call notificationService.requestPermission() here.
|
// IMPORTANT: do NOT call notificationService.requestPermission() here.
|
||||||
// Browsers (Chrome/Firefox) require permission requests to come from
|
// Browsers (Chrome/Firefox) require permission requests to come from
|
||||||
// a user gesture. Calling it at mount time queues the prompt until
|
// a user gesture. Calling it at mount time queues the prompt until
|
||||||
|
|
@ -628,8 +701,9 @@
|
||||||
appName="Mana"
|
appName="Mana"
|
||||||
locale={($locale || 'de') === 'de' ? 'de' : 'en'}
|
locale={($locale || 'de') === 'de' ? 'de' : 'en'}
|
||||||
>
|
>
|
||||||
<!-- Onboarding Wizard (auth only) -->
|
<!-- Onboarding Wizard (auth only) — loaded on demand -->
|
||||||
{#if showOnboarding && authStore.isAuthenticated}
|
{#if showOnboarding && authStore.isAuthenticated && OnboardingWizardC}
|
||||||
|
{@const OnboardingWizard = OnboardingWizardC}
|
||||||
<div
|
<div
|
||||||
class="fixed inset-0 z-50 flex items-center justify-center bg-background/95 backdrop-blur-sm"
|
class="fixed inset-0 z-50 flex items-center justify-center bg-background/95 backdrop-blur-sm"
|
||||||
>
|
>
|
||||||
|
|
@ -654,10 +728,14 @@
|
||||||
|
|
||||||
<!-- One-time encryption intro — sits at the top of the stack so
|
<!-- One-time encryption intro — sits at the top of the stack so
|
||||||
it can't be obscured by the QuickInputBar / TagStrip / PillNav.
|
it can't be obscured by the QuickInputBar / TagStrip / PillNav.
|
||||||
Self-gates on isVaultUnlocked() so guests never see it. -->
|
Self-gates on isVaultUnlocked() so guests never see it.
|
||||||
<div class="bottom-stack-notification">
|
Lazy-loaded after idle (see $effects above). -->
|
||||||
<EncryptionIntroBanner />
|
{#if EncryptionIntroBannerC}
|
||||||
</div>
|
{@const EncryptionIntroBanner = EncryptionIntroBannerC}
|
||||||
|
<div class="bottom-stack-notification">
|
||||||
|
<EncryptionIntroBanner />
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
<!-- Sync pause banner — shown when sync was paused due to insufficient credits -->
|
<!-- Sync pause banner — shown when sync was paused due to insufficient credits -->
|
||||||
{#if syncBilling.paused}
|
{#if syncBilling.paused}
|
||||||
|
|
@ -694,8 +772,10 @@
|
||||||
|
|
||||||
<!-- Session expiry warning (auth only). Self-gates on the
|
<!-- Session expiry warning (auth only). Self-gates on the
|
||||||
secondsLeft countdown and only renders inside the stack
|
secondsLeft countdown and only renders inside the stack
|
||||||
when actually warning, so the wrapper is no-op otherwise. -->
|
when actually warning, so the wrapper is no-op otherwise.
|
||||||
{#if authStore.isAuthenticated}
|
Lazy-loaded after idle. -->
|
||||||
|
{#if authStore.isAuthenticated && SessionWarningC}
|
||||||
|
{@const SessionWarning = SessionWarningC}
|
||||||
<div class="bottom-stack-notification">
|
<div class="bottom-stack-notification">
|
||||||
<SessionWarning />
|
<SessionWarning />
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -704,16 +784,23 @@
|
||||||
<!-- Cross-module automation suggestions. Lives in the (app)
|
<!-- Cross-module automation suggestions. Lives in the (app)
|
||||||
stack because automationsStore is an (app)-only module
|
stack because automationsStore is an (app)-only module
|
||||||
and the toast doesn't make sense on auth/landing pages
|
and the toast doesn't make sense on auth/landing pages
|
||||||
anyway. Self-gates on visible state. -->
|
anyway. Self-gates on visible state. Lazy-loaded after idle. -->
|
||||||
<div class="bottom-stack-notification">
|
{#if SuggestionToastC}
|
||||||
<SuggestionToast />
|
{@const SuggestionToast = SuggestionToastC}
|
||||||
</div>
|
<div class="bottom-stack-notification">
|
||||||
|
<SuggestionToast />
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
<!-- Companion Brain pulse nudges — water reminders, streak
|
<!-- Companion Brain pulse nudges — water reminders, streak
|
||||||
warnings, morning summary etc. Self-gates on active nudges. -->
|
warnings, morning summary etc. Self-gates on active nudges.
|
||||||
<div class="bottom-stack-notification">
|
Lazy-loaded after idle. -->
|
||||||
<NudgeToast />
|
{#if NudgeToastC}
|
||||||
</div>
|
{@const NudgeToast = NudgeToastC}
|
||||||
|
<div class="bottom-stack-notification">
|
||||||
|
<NudgeToast />
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
<!-- QuickInputBar with inline nav toggle — gated by the "search" pill -->
|
<!-- QuickInputBar with inline nav toggle — gated by the "search" pill -->
|
||||||
{#if isQuickInputVisible}
|
{#if isQuickInputVisible}
|
||||||
|
|
@ -882,8 +969,11 @@
|
||||||
so it doesn't end up obscured by the QuickInputBar like
|
so it doesn't end up obscured by the QuickInputBar like
|
||||||
EncryptionIntroBanner used to be. -->
|
EncryptionIntroBanner used to be. -->
|
||||||
|
|
||||||
<!-- Keyboard shortcuts modal -->
|
<!-- Keyboard shortcuts modal — loaded on first `?` press -->
|
||||||
<KeyboardShortcutsModal open={showShortcuts} onclose={() => (showShortcuts = false)} />
|
{#if KeyboardShortcutsModalC}
|
||||||
|
{@const KeyboardShortcutsModal = KeyboardShortcutsModalC}
|
||||||
|
<KeyboardShortcutsModal open={showShortcuts} onclose={() => (showShortcuts = false)} />
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Navigation Context Menu -->
|
<!-- Navigation Context Menu -->
|
||||||
|
|
@ -895,8 +985,9 @@
|
||||||
onClose={() => navCtxMenu.close()}
|
onClose={() => navCtxMenu.close()}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- Guest Welcome Modal -->
|
<!-- Guest Welcome Modal — loaded when guest mode activates -->
|
||||||
{#if guestMode}
|
{#if guestMode && GuestWelcomeModalC}
|
||||||
|
{@const GuestWelcomeModal = GuestWelcomeModalC}
|
||||||
<GuestWelcomeModal
|
<GuestWelcomeModal
|
||||||
appId="mana"
|
appId="mana"
|
||||||
visible={guestMode.showWelcome}
|
visible={guestMode.showWelcome}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue