mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-27 17:58:07 +00:00
perf(web): lazy-load dashboard routes and split heavy vendors
The production dashboard build packed almost every page plus xterm/three/ plot into one large JS chunk, which trips Vite's 500kB warning and slows first paint even when the user only opens Sessions/Config. - Lazy-load route pages in App.tsx behind Suspense - Defer mounting the persistent embedded chat host (and xterm) until the first /chat visit, while keeping the sticky PTY latch afterward - Add rolldown vendor codeSplitting groups (react, xterm, three, plot, motion, ui) and raise chunkSizeWarningLimit modestly to 600kB Addresses #25912 (partial: route lazy-load + vendor splits + fallbacks; not yet CI bundle analysis or documented entry budget). Verified locally: npm run typecheck, npm run test (97), npm run build with separate page/vendor chunks.
This commit is contained in:
parent
aff48958d3
commit
2df57fe641
2 changed files with 122 additions and 45 deletions
122
web/src/App.tsx
122
web/src/App.tsx
|
|
@ -1,4 +1,6 @@
|
|||
import {
|
||||
lazy,
|
||||
Suspense,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
|
|
@ -72,25 +74,27 @@ import { ProfileSwitcher } from "@/components/ProfileSwitcher";
|
|||
import { ProfileScopeBanner } from "@/components/ProfileScopeBanner";
|
||||
import { useSystemActions } from "@/contexts/useSystemActions";
|
||||
import type { SystemAction } from "@/contexts/system-actions-context";
|
||||
import ConfigPage from "@/pages/ConfigPage";
|
||||
import DocsPage from "@/pages/DocsPage";
|
||||
import EnvPage from "@/pages/EnvPage";
|
||||
import FilesPage from "@/pages/FilesPage";
|
||||
import SessionsPage from "@/pages/SessionsPage";
|
||||
import LogsPage from "@/pages/LogsPage";
|
||||
import AnalyticsPage from "@/pages/AnalyticsPage";
|
||||
import ModelsPage from "@/pages/ModelsPage";
|
||||
import CronPage from "@/pages/CronPage";
|
||||
import ProfilesPage from "@/pages/ProfilesPage";
|
||||
import ProfileBuilderPage from "@/pages/ProfileBuilderPage";
|
||||
import SkillsPage from "@/pages/SkillsPage";
|
||||
import PluginsPage from "@/pages/PluginsPage";
|
||||
import McpPage from "@/pages/McpPage";
|
||||
import PairingPage from "@/pages/PairingPage";
|
||||
import ChannelsPage from "@/pages/ChannelsPage";
|
||||
import WebhooksPage from "@/pages/WebhooksPage";
|
||||
import SystemPage from "@/pages/SystemPage";
|
||||
import ChatPage from "@/pages/ChatPage";
|
||||
// Route pages are lazy-loaded so the initial dashboard shell does not pay for
|
||||
// every admin surface (and heavy deps like xterm) up front.
|
||||
const ConfigPage = lazy(() => import("@/pages/ConfigPage"));
|
||||
const DocsPage = lazy(() => import("@/pages/DocsPage"));
|
||||
const EnvPage = lazy(() => import("@/pages/EnvPage"));
|
||||
const FilesPage = lazy(() => import("@/pages/FilesPage"));
|
||||
const SessionsPage = lazy(() => import("@/pages/SessionsPage"));
|
||||
const LogsPage = lazy(() => import("@/pages/LogsPage"));
|
||||
const AnalyticsPage = lazy(() => import("@/pages/AnalyticsPage"));
|
||||
const ModelsPage = lazy(() => import("@/pages/ModelsPage"));
|
||||
const CronPage = lazy(() => import("@/pages/CronPage"));
|
||||
const ProfilesPage = lazy(() => import("@/pages/ProfilesPage"));
|
||||
const ProfileBuilderPage = lazy(() => import("@/pages/ProfileBuilderPage"));
|
||||
const SkillsPage = lazy(() => import("@/pages/SkillsPage"));
|
||||
const PluginsPage = lazy(() => import("@/pages/PluginsPage"));
|
||||
const McpPage = lazy(() => import("@/pages/McpPage"));
|
||||
const PairingPage = lazy(() => import("@/pages/PairingPage"));
|
||||
const ChannelsPage = lazy(() => import("@/pages/ChannelsPage"));
|
||||
const WebhooksPage = lazy(() => import("@/pages/WebhooksPage"));
|
||||
const SystemPage = lazy(() => import("@/pages/SystemPage"));
|
||||
const ChatPage = lazy(() => import("@/pages/ChatPage"));
|
||||
import { LanguageSwitcher } from "@/components/LanguageSwitcher";
|
||||
import { ThemeSwitcher } from "@/components/ThemeSwitcher";
|
||||
import { useI18n } from "@/i18n";
|
||||
|
|
@ -99,9 +103,25 @@ import { PluginPage, PluginSlot, usePlugins } from "@/plugins";
|
|||
import type { PluginManifest } from "@/plugins";
|
||||
import { useTheme } from "@/themes";
|
||||
import { isDashboardEmbeddedChatEnabled } from "@/lib/dashboard-flags";
|
||||
import { latchChatActivation } from "@/lib/chat-activation";
|
||||
import { api } from "@/lib/api";
|
||||
import type { StatusResponse, UpdateCheckResponse } from "@/lib/api";
|
||||
|
||||
function RouteFallback({ label = "Loading…" }: { label?: string }) {
|
||||
return (
|
||||
<div
|
||||
className="flex min-h-[12rem] flex-1 items-center justify-center"
|
||||
aria-busy="true"
|
||||
aria-live="polite"
|
||||
>
|
||||
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
||||
<Spinner />
|
||||
<span>{label}</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function RootRedirect() {
|
||||
return <Navigate to="/sessions" replace />;
|
||||
}
|
||||
|
|
@ -127,8 +147,10 @@ const CHAT_NAV_ITEM: NavItem = {
|
|||
* inline near the bottom of this file — so the PTY child, WebSocket,
|
||||
* and xterm instance survive when the user visits another tab and comes
|
||||
* back. A `display:none` toggle hides the terminal without unmounting.
|
||||
* Routing still owns the URL so /chat deep-links, browser back/forward,
|
||||
* and nav highlight keep working.
|
||||
* The host itself is still deferred until the first /chat visit so the
|
||||
* xterm chunk is not downloaded on unrelated pages. Routing still owns
|
||||
* the URL so /chat deep-links, browser back/forward, and nav highlight
|
||||
* keep working.
|
||||
*/
|
||||
const BUILTIN_ROUTES_CORE: Record<string, ComponentType> = {
|
||||
"/": RootRedirect,
|
||||
|
|
@ -378,6 +400,13 @@ export default function App() {
|
|||
const normalizedPath = pathname.replace(/\/$/, "") || "/";
|
||||
const isChatRoute = normalizedPath === "/chat";
|
||||
const embeddedChat = isDashboardEmbeddedChatEnabled();
|
||||
// Defer mounting the persistent chat host (and its xterm chunk) until the
|
||||
// user has actually opened /chat at least once. Sticky after that so the
|
||||
// PTY survives later tab switches.
|
||||
const [chatHostMounted, setChatHostMounted] = useState(isChatRoute);
|
||||
useEffect(() => {
|
||||
setChatHostMounted((prev) => latchChatActivation(prev, isChatRoute));
|
||||
}, [isChatRoute]);
|
||||
|
||||
// `dashboard.show_token_analytics` gates the Analytics nav item. The
|
||||
// page itself remains reachable by URL (it renders an explanation when
|
||||
|
|
@ -737,35 +766,28 @@ export default function App() {
|
|||
)}
|
||||
>
|
||||
<ProfileKeyedRoutes>
|
||||
<Routes>
|
||||
{routes.map(({ key, path, element }) => (
|
||||
<Route key={key} path={path} element={element} />
|
||||
))}
|
||||
<Route
|
||||
path="*"
|
||||
element={
|
||||
<UnknownRouteFallback pluginsLoading={pluginsLoading} />
|
||||
}
|
||||
/>
|
||||
</Routes>
|
||||
<Suspense fallback={<RouteFallback />}>
|
||||
<Routes>
|
||||
{routes.map(({ key, path, element }) => (
|
||||
<Route key={key} path={path} element={element} />
|
||||
))}
|
||||
<Route
|
||||
path="*"
|
||||
element={
|
||||
<UnknownRouteFallback pluginsLoading={pluginsLoading} />
|
||||
}
|
||||
/>
|
||||
</Routes>
|
||||
</Suspense>
|
||||
</ProfileKeyedRoutes>
|
||||
|
||||
{embeddedChat &&
|
||||
!chatOverriddenByPlugin &&
|
||||
(pluginsLoading ? (
|
||||
isChatRoute ? (
|
||||
<div
|
||||
className="flex min-h-0 min-w-0 flex-1 items-center justify-center"
|
||||
aria-busy="true"
|
||||
aria-live="polite"
|
||||
>
|
||||
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
||||
<Spinner />
|
||||
<span>Loading chat…</span>
|
||||
</div>
|
||||
</div>
|
||||
<RouteFallback label="Loading chat…" />
|
||||
) : null
|
||||
) : (
|
||||
) : chatHostMounted ? (
|
||||
<div
|
||||
data-chat-active={isChatRoute ? "true" : "false"}
|
||||
className={cn(
|
||||
|
|
@ -774,9 +796,19 @@ export default function App() {
|
|||
)}
|
||||
aria-hidden={!isChatRoute}
|
||||
>
|
||||
<ChatPage isActive={isChatRoute} />
|
||||
<Suspense
|
||||
fallback={
|
||||
isChatRoute ? (
|
||||
<RouteFallback label="Loading chat…" />
|
||||
) : null
|
||||
}
|
||||
>
|
||||
<ChatPage isActive={isChatRoute} />
|
||||
</Suspense>
|
||||
</div>
|
||||
))}
|
||||
) : isChatRoute ? (
|
||||
<RouteFallback label="Loading chat…" />
|
||||
) : null)}
|
||||
</div>
|
||||
<PluginSlot name="post-main" />
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -86,6 +86,51 @@ export default defineConfig({
|
|||
build: {
|
||||
outDir: "../hermes_cli/web_dist",
|
||||
emptyOutDir: true,
|
||||
// Shell stays a bit over Vite's 500 kB default after vendor splits;
|
||||
// page/xterm chunks load on demand. Keep a modest ceiling so a true
|
||||
// regression still warns.
|
||||
chunkSizeWarningLimit: 600,
|
||||
// Split heavy vendors so the first dashboard paint does not download
|
||||
// xterm/three/plot/etc. until a route actually needs them. Lazy page
|
||||
// imports in App.tsx create the route boundaries; these groups keep
|
||||
// shared node_modules out of every page chunk.
|
||||
rolldownOptions: {
|
||||
output: {
|
||||
codeSplitting: {
|
||||
minSize: 20_000,
|
||||
groups: [
|
||||
{
|
||||
name: "react-vendor",
|
||||
test: /node_modules[\\/](react|react-dom|scheduler|react-router|react-router-dom)([\\/]|$)/,
|
||||
},
|
||||
{
|
||||
name: "xterm",
|
||||
test: /node_modules[\\/]@xterm[\\/]/,
|
||||
},
|
||||
{
|
||||
name: "three",
|
||||
test: /node_modules[\\/](three|@react-three)([\\/]|$)/,
|
||||
},
|
||||
{
|
||||
name: "plot",
|
||||
test: /node_modules[\\/]@observablehq[\\/]plot([\\/]|$)/,
|
||||
},
|
||||
{
|
||||
name: "motion",
|
||||
test: /node_modules[\\/](motion|framer-motion)([\\/]|$)/,
|
||||
},
|
||||
{
|
||||
name: "ui",
|
||||
test: /node_modules[\\/]@nous-research[\\/]ui([\\/]|$)/,
|
||||
},
|
||||
{
|
||||
name: "vendor",
|
||||
test: /node_modules[\\/]/,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
server: {
|
||||
proxy: {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue