tenseleyflow/claudex / 19ab69c

Browse files

perf: code-split terminal chunk behind react lazy+suspense

Authored by espadonne
SHA
19ab69c2bfc5b30ed17c274489e30c8b8009445a
Parents
d2d4313
Tree
74fc257

1 changed file

StatusFile+-
M src/components/ViewerPane.tsx 33 8
src/components/ViewerPane.tsxmodified
@@ -1,15 +1,23 @@
1
-import { useMemo } from "react";
1
+import { lazy, Suspense, useMemo } from "react";
22
 
33
 import { ArchiveChatBanner } from "@/components/ArchiveChatBanner";
44
 import { ChatInput } from "@/components/ChatInput";
55
 import { MessageTimeline } from "@/components/MessageTimeline";
6
-import { TerminalPane } from "@/components/TerminalPane";
76
 import { TurnStatusBanner } from "@/components/TurnStatusBanner";
87
 import { PaneHeader } from "@/components/panes/PaneHeader";
98
 import { shortModel } from "@/lib/format";
109
 import { useSessionStore } from "@/lib/store/sessions";
1110
 import type { SessionSummary } from "@/lib/ipc/types";
1211
 
12
+// Code-split xterm. The terminal stack (xterm core + fit + webgl +
13
+// unicode11 + web-links) is ~230 KB gzipped — that's a third of the
14
+// total JS bundle. Lazy-loading keeps it out of initial paint so
15
+// the app starts faster, and users who never open a terminal
16
+// session never pay the cost at all.
17
+const TerminalPane = lazy(() =>
18
+  import("@/components/TerminalPane").then((m) => ({ default: m.TerminalPane })),
19
+);
20
+
1321
 export function ViewerPane() {
1422
   const detail = useSessionStore((s) => s.detail);
1523
   const loading = useSessionStore((s) => s.loading.detail);
@@ -125,12 +133,18 @@ export function ViewerPane() {
125133
         // key forces a fresh mount per session — xterm state is
126134
         // bound to sessionId so switching sessions must teardown
127135
         // and re-open (the backend PTY keeps running regardless).
128
-        <TerminalPane
129
-          key={summary.id}
130
-          sessionId={summary.id}
131
-          cwd={summary.cwd}
132
-          claudeArgs={claudeArgs}
133
-        />
136
+        //
137
+        // Suspense fallback renders while the lazy chunk downloads
138
+        // on first use. Once loaded, the chunk is cached so
139
+        // subsequent mounts are instant.
140
+        <Suspense fallback={<TerminalLoading />}>
141
+          <TerminalPane
142
+            key={summary.id}
143
+            sessionId={summary.id}
144
+            cwd={summary.cwd}
145
+            claudeArgs={claudeArgs}
146
+          />
147
+        </Suspense>
134148
       ) : showingSkeleton ? (
135149
         <CardsSkeleton />
136150
       ) : detail ? (
@@ -152,6 +166,17 @@ export function ViewerPane() {
152166
   );
153167
 }
154168
 
169
+/** Tiny placeholder for the lazy TerminalPane chunk. Only visible
170
+ *  on the first-ever terminal mount in a session — subsequent
171
+ *  mounts reuse the cached module and render instantly. */
172
+function TerminalLoading() {
173
+  return (
174
+    <div className="flex h-full items-center justify-center text-[11px] text-fg-3">
175
+      loading terminal…
176
+    </div>
177
+  );
178
+}
179
+
155180
 /** Shimmer placeholder shown while a session detail is loading.
156181
  *  Six ghost message cards, sized similarly to real messages, with
157182
  *  a subtle animation. Keeps the viewer feeling alive instead of