515c08afed
Everything sat in the eager startup graph (App.tsx statically imported all 28
routes; the editor pulled TipTap + KaTeX + ~45 lowlight grammars + drawio;
posthog + AI SDK loaded for everyone) — a /login visitor downloaded+compiled the
whole editor. Client-only; functionality 1:1, only WHEN code loads changed.
Result (prod build): eager JS 3.5MB -> ~1.12MB, entry 1920KB -> 552KB; KaTeX
(250KB) and the TipTap engine (~586KB) are now lazy chunks, off the startup path.
- App.tsx: route-level React.lazy + Suspense (editor Page, all settings/*, share,
space/home routes). Auth/redirect/cold-start routes stay eager. Suspense lives
inside Layout/ShareLayout around the Outlet so the shell stays mounted.
- Lazy KaTeX node views (math-inline-lazy/math-block-lazy) + lazy drawio
(drawio-view-lazy/drawio-menu-lazy), mirroring mermaid/excalidraw, each with a
node-sized Suspense placeholder so a slow chunk can't crash the editor.
- posthog-js is now a conditional dynamic import under the unchanged
isCloud() && isPostHogEnabled gate — self-hosted never downloads it.
- AiChatWindow is React.lazy, mounted on first open and kept mounted (a live AI
stream isn't torn down); renders null while closed (identical behavior).
- Cut eager TipTap pulls from always-loaded shell modules: editor-atoms /
global-bridge Editor -> import type; Aside lazily loaded (page routes only);
config.ts sanitizeUrl and use-clipboard execCommandCopy moved to client-local
src/lib/{sanitize-url,copy-to-clipboard}.ts (byte-identical to the editor-ext
originals, dropping the barrel's top-level @tiptap import); WebSocketStatus
import replaced with the "connected" literal the status atom already stores.
- vite.config.ts: a vendor-katex chunk group (TipTap/PM/Yjs intentionally NOT
grouped — grouping dragged the engine eager; documented in the config).
- lowlight grammar registration is left inside the (now-lazy) editor chunk:
listLanguages()/highlighting are synchronous, so deferring registration would
change behavior for marginal in-chunk gain — the route split already removes it
from startup, which was the complaint.
Gate: client build succeeds, tsc --noEmit clean, frozen install EXIT 0 (added
@braintree/sanitize-url as a direct client dep + regenerated the lock).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
123 lines
5.2 KiB
TypeScript
123 lines
5.2 KiB
TypeScript
import { lazy, Suspense } from "react";
|
|
import { Navigate, Route, Routes } from "react-router-dom";
|
|
import { Center, Loader } from "@mantine/core";
|
|
import { Error404 } from "@/components/ui/error-404.tsx";
|
|
import Layout from "@/components/layouts/global/layout.tsx";
|
|
import { useTrackOrigin } from "@/hooks/use-track-origin";
|
|
|
|
// ShareLayout is route-split: its ShareShell chrome pulls in the table of
|
|
// contents (and thus TipTap), so keeping it out of the eager graph removes the
|
|
// editor engine from startup for authenticated users too.
|
|
const ShareLayout = lazy(
|
|
() => import("@/features/share/components/share-layout.tsx"),
|
|
);
|
|
|
|
// Auth / entry pages stay eager: they are the first paint for an unauthenticated
|
|
// visitor (e.g. /login) and are already small, so code-splitting them would only
|
|
// add a cold-chunk round trip to the most common cold-start path.
|
|
import SetupWorkspace from "@/pages/auth/setup-workspace.tsx";
|
|
import LoginPage from "@/pages/auth/login";
|
|
import InviteSignup from "@/pages/auth/invite-signup.tsx";
|
|
import ForgotPassword from "@/pages/auth/forgot-password.tsx";
|
|
import PasswordReset from "./pages/auth/password-reset";
|
|
import PageRedirect from "@/pages/page/page-redirect.tsx";
|
|
import ShareRedirect from "@/pages/share/share-redirect.tsx";
|
|
|
|
// Heavy / leaf pages are route-split with React.lazy so their code (most
|
|
// importantly the whole TipTap editor + KaTeX + lowlight grammars + drawio that
|
|
// the page editor and the readonly share editor pull in) is fetched only when
|
|
// the matching route is actually visited. The <Suspense> boundaries live inside
|
|
// each Layout (around its <Outlet/>), so the app shell stays mounted while a
|
|
// route chunk loads.
|
|
const Home = lazy(() => import("@/pages/dashboard/home"));
|
|
const Page = lazy(() => import("@/pages/page/page"));
|
|
const SpaceHome = lazy(() => import("@/pages/space/space-home.tsx"));
|
|
const SpaceTrash = lazy(() => import("@/pages/space/space-trash.tsx"));
|
|
const SpacesPage = lazy(() => import("@/pages/spaces/spaces.tsx"));
|
|
const FavoritesPage = lazy(() => import("@/pages/favorites/favorites-page"));
|
|
const LabelPage = lazy(() => import("@/pages/label/label-page"));
|
|
const SharedPage = lazy(() => import("@/pages/share/shared-page.tsx"));
|
|
|
|
const AccountSettings = lazy(
|
|
() => import("@/pages/settings/account/account-settings"),
|
|
);
|
|
const AccountPreferences = lazy(
|
|
() => import("@/pages/settings/account/account-preferences.tsx"),
|
|
);
|
|
const WorkspaceSettings = lazy(
|
|
() => import("@/pages/settings/workspace/workspace-settings"),
|
|
);
|
|
const AiSettings = lazy(() => import("@/pages/settings/workspace/ai-settings"));
|
|
const WorkspaceMembers = lazy(
|
|
() => import("@/pages/settings/workspace/workspace-members"),
|
|
);
|
|
const Groups = lazy(() => import("@/pages/settings/group/groups"));
|
|
const GroupInfo = lazy(() => import("./pages/settings/group/group-info"));
|
|
const Spaces = lazy(() => import("@/pages/settings/space/spaces.tsx"));
|
|
const Shares = lazy(() => import("@/pages/settings/shares/shares.tsx"));
|
|
|
|
export default function App() {
|
|
useTrackOrigin();
|
|
|
|
return (
|
|
<Suspense
|
|
fallback={
|
|
<Center h="100vh">
|
|
<Loader size="sm" />
|
|
</Center>
|
|
}
|
|
>
|
|
<Routes>
|
|
<Route index element={<Navigate to="/home" />} />
|
|
<Route path={"/login"} element={<LoginPage />} />
|
|
<Route path={"/invites/:invitationId"} element={<InviteSignup />} />
|
|
<Route path={"/forgot-password"} element={<ForgotPassword />} />
|
|
<Route path={"/password-reset"} element={<PasswordReset />} />
|
|
|
|
<Route path={"/setup/register"} element={<SetupWorkspace />} />
|
|
|
|
<Route element={<ShareLayout />}>
|
|
<Route
|
|
path={"/share/:shareId/p/:pageSlug"}
|
|
element={<SharedPage />}
|
|
/>
|
|
<Route path={"/share/p/:pageSlug"} element={<SharedPage />} />
|
|
</Route>
|
|
|
|
<Route path={"/share/:shareId"} element={<ShareRedirect />} />
|
|
<Route path={"/p/:pageSlug"} element={<PageRedirect />} />
|
|
|
|
<Route element={<Layout />}>
|
|
<Route path={"/home"} element={<Home />} />
|
|
<Route path={"/spaces"} element={<SpacesPage />} />
|
|
<Route path={"/favorites"} element={<FavoritesPage />} />
|
|
<Route path={"/labels/:labelName"} element={<LabelPage />} />
|
|
<Route path={"/s/:spaceSlug"} element={<SpaceHome />} />
|
|
<Route path={"/s/:spaceSlug/trash"} element={<SpaceTrash />} />
|
|
<Route
|
|
path={"/s/:spaceSlug/p/:pageSlug"}
|
|
element={<Page />}
|
|
/>
|
|
|
|
<Route path={"/settings"}>
|
|
<Route path={"account/profile"} element={<AccountSettings />} />
|
|
<Route
|
|
path={"account/preferences"}
|
|
element={<AccountPreferences />}
|
|
/>
|
|
<Route path={"workspace"} element={<WorkspaceSettings />} />
|
|
<Route path={"ai"} element={<AiSettings />} />
|
|
<Route path={"members"} element={<WorkspaceMembers />} />
|
|
<Route path={"groups"} element={<Groups />} />
|
|
<Route path={"groups/:groupId"} element={<GroupInfo />} />
|
|
<Route path={"spaces"} element={<Spaces />} />
|
|
<Route path={"sharing"} element={<Shares />} />
|
|
</Route>
|
|
</Route>
|
|
|
|
<Route path="*" element={<Error404 />} />
|
|
</Routes>
|
|
</Suspense>
|
|
);
|
|
}
|