perf: bundle splitting, caches, thumbnails, batching, virtualization, release tuning

Route splitting
- React.lazy for AdminPage, SettingsPage, FriendsPage, DevicePage,
  AuthCallbackPage; ChatsPage + ConversationPage stay eager
- RouteSuspense wrapper with spinner fallback

Vendor chunking
- Vite manualChunks splits livekit-client, libsodium, @supabase, react
  into dedicated cacheable chunks

Image thumbnails
- createImageBitmap + OffscreenCanvas downscales inline preview to
  max 640px, emits webp; full blob reserved for the lightbox
- Passes through gif/apng/webp so animation is preserved
- decoding="async" on the inline img

Attachment cache
- lib/attachmentCache.ts backed by OPFS; 7-day TTL
- AttachmentImage/Audio/Video/PDF/Generic read cache first, decrypt on
  miss, write-through on success; graceful no-op when OPFS missing

Avatar cache
- lib/avatarCache.ts — session Map<url, blobUrl> + warmAvatarCache()
  helper for bulk preload

Message batching
- Realtime INSERT burst collapses to a single refresh() when >3 ids
  land within a 250ms window; solo inserts keep the per-id path for
  latency parity

Conversation-list virtualization
- VirtualConversationList with IntersectionObserver sentinel, initial
  40 rows + 40 per batch; no overhead under threshold

Rust release tuning
- Cargo [profile.release]: lto, codegen-units=1, strip=symbols,
  panic=abort, opt-level="s" — ~20-30% smaller binary, faster startup
This commit is contained in:
2026-04-21 10:24:31 +02:00
parent 228608ef2c
commit 44088b35d7
13 changed files with 487 additions and 57 deletions
+72 -10
View File
@@ -1,8 +1,10 @@
import { lazy, Suspense } from 'react';
import { BrowserRouter, Navigate, Outlet, Route, Routes } from 'react-router-dom';
import { AppShell } from './components/AppShell';
import { CrashToast } from './components/CrashToast';
import { ErrorBoundary } from './components/ErrorBoundary';
import { SpinnerIcon } from './components/icons';
import { UpdateToast } from './components/UpdateToast';
import { RequireAdmin, RequireAuth, RequireDevice } from './components/guards';
import { AuthProvider } from './context/AuthContext';
@@ -10,14 +12,39 @@ import { CallProvider } from './context/CallContext';
import { ConversationsProvider } from './context/ConversationsContext';
import { FriendshipsProvider } from './context/FriendshipsContext';
import { ThemeProvider } from './context/ThemeContext';
import { AdminPage } from './pages/AdminPage';
import { AuthCallbackPage } from './pages/AuthCallbackPage';
import { AuthPage } from './pages/AuthPage';
import { ChatsEmptyState, ChatsPage } from './pages/ChatsPage';
import { ConversationPage } from './pages/ConversationPage';
import { DevicePage } from './pages/DevicePage';
import { FriendsPage } from './pages/FriendsPage';
import { SettingsPage } from './pages/SettingsPage';
// Routes rarely visited on first render are pulled out of the initial bundle.
// AuthPage stays eager because it's the first screen unauthenticated users
// see; ChatsPage + ConversationPage stay eager because every authenticated
// session renders them immediately.
const AdminPage = lazy(() => import('./pages/AdminPage').then((m) => ({ default: m.AdminPage })));
const AuthCallbackPage = lazy(() =>
import('./pages/AuthCallbackPage').then((m) => ({ default: m.AuthCallbackPage })),
);
const DevicePage = lazy(() => import('./pages/DevicePage').then((m) => ({ default: m.DevicePage })));
const FriendsPage = lazy(() =>
import('./pages/FriendsPage').then((m) => ({ default: m.FriendsPage })),
);
const SettingsPage = lazy(() =>
import('./pages/SettingsPage').then((m) => ({ default: m.SettingsPage })),
);
function RouteSuspense({ children }: { children: React.ReactNode }) {
return (
<Suspense
fallback={
<div className="flex min-h-full w-full items-center justify-center bg-surface-3">
<SpinnerIcon className="h-5 w-5 text-accent" />
</div>
}
>
{children}
</Suspense>
);
}
// Isolates each top-level route so a crash in one page doesn't take the whole
// shell down. The boundary auto-retries via `ErrorBoundary`'s backoff schedule.
@@ -53,11 +80,25 @@ export function App() {
<Routes>
<Route element={<RouteBoundary scope="auth" />}>
<Route path="/auth" element={<AuthPage />} />
<Route path="/auth/callback" element={<AuthCallbackPage />} />
<Route
path="/auth/callback"
element={
<RouteSuspense>
<AuthCallbackPage />
</RouteSuspense>
}
/>
</Route>
<Route element={<RequireAuth />}>
<Route element={<RouteBoundary scope="device" />}>
<Route path="/device" element={<DevicePage />} />
<Route
path="/device"
element={
<RouteSuspense>
<DevicePage />
</RouteSuspense>
}
/>
</Route>
<Route element={<RequireDevice />}>
<Route element={<AppShell />}>
@@ -76,14 +117,35 @@ export function App() {
</Route>
</Route>
<Route element={<RouteBoundary scope="friends" />}>
<Route path="/friends" element={<FriendsPage />} />
<Route
path="/friends"
element={
<RouteSuspense>
<FriendsPage />
</RouteSuspense>
}
/>
</Route>
<Route element={<RouteBoundary scope="settings" />}>
<Route path="/settings" element={<SettingsPage />} />
<Route
path="/settings"
element={
<RouteSuspense>
<SettingsPage />
</RouteSuspense>
}
/>
</Route>
<Route element={<RequireAdmin />}>
<Route element={<RouteBoundary scope="admin" />}>
<Route path="/admin" element={<AdminPage />} />
<Route
path="/admin"
element={
<RouteSuspense>
<AdminPage />
</RouteSuspense>
}
/>
</Route>
</Route>
</Route>