mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-08-27 18:57:09 +00:00
perf(frontend): lazy-load xterm chunk + addons (#825)
xterm-the-terminal-emulator and its three addons (fit, search, serialize) used to be imported at module scope by Terminal.tsx, BashExecModal.tsx, and HostConsole.tsx along with xterm's CSS. Even though only Terminal.tsx is rendered eagerly inside the editor layout, the static imports forced the ~660 KB xterm chunk plus the xterm.css bytes into every cold app start regardless of whether a user ever opened a terminal. Move the bootstrap into a new frontend/src/lib/xtermLoader.ts module. loadXtermModules() Promise.alls the four addon imports plus the CSS, caches the result on a shared promise, and returns the constructors. On rejection the cache is cleared so the next mount can retry instead of rethrowing the same failed promise. Three consumers (Terminal, BashExecModal, HostConsole) swap their value imports for type-only InstanceType aliases from the loader, then call loadXtermModules() inside their existing useEffect. A mounted/cancelled flag in each effect closure prevents initialisation if the component unmounts during the load. The vite.config.ts manualChunks group from #823 already groups all @xterm/* packages into the xterm chunk, so it now loads on demand instead of being bundled into the entry chunk.
This commit is contained in:
@@ -1,9 +1,7 @@
|
||||
import { useEffect, useRef, useState, useCallback } from 'react';
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from './ui/dialog';
|
||||
import { Terminal as TerminalIcon } from 'lucide-react';
|
||||
import { Terminal } from '@xterm/xterm';
|
||||
import { FitAddon } from '@xterm/addon-fit';
|
||||
import '@xterm/xterm/css/xterm.css';
|
||||
import { loadXtermModules, type Terminal, type FitAddon, type XtermModules } from '@/lib/xtermLoader';
|
||||
|
||||
type TerminalContainer = HTMLDivElement & { __resizeObserver?: ResizeObserver };
|
||||
|
||||
@@ -45,10 +43,13 @@ export default function BashExecModal({ isOpen, onClose, containerId, containerN
|
||||
return;
|
||||
}
|
||||
|
||||
let cancelled = false;
|
||||
let attempts = 0;
|
||||
const maxAttempts = 15; // up to 1.5 seconds
|
||||
let modules: XtermModules | null = null;
|
||||
|
||||
const checkAndInit = () => {
|
||||
if (cancelled || !modules) return;
|
||||
// If already initialized, stop.
|
||||
if (xtermRef.current) return;
|
||||
|
||||
@@ -72,23 +73,28 @@ export default function BashExecModal({ isOpen, onClose, containerId, containerN
|
||||
initTimeoutRef.current = setTimeout(checkAndInit, 100);
|
||||
} else {
|
||||
console.warn('BashExecModal: terminal container has zero dimensions after 1.5s, forcing init anyway.');
|
||||
initTerminal(container);
|
||||
initTerminal(container, modules);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Node exists and has layout - safe to initialize xterm!
|
||||
initTerminal(container);
|
||||
initTerminal(container, modules);
|
||||
};
|
||||
|
||||
// Start polling
|
||||
initTimeoutRef.current = setTimeout(checkAndInit, 50);
|
||||
void loadXtermModules().then((mods) => {
|
||||
if (cancelled) return;
|
||||
modules = mods;
|
||||
initTimeoutRef.current = setTimeout(checkAndInit, 50);
|
||||
}).catch((err) => {
|
||||
console.error('BashExecModal: failed to load xterm:', err);
|
||||
});
|
||||
|
||||
function initTerminal(containerEl: HTMLDivElement) {
|
||||
function initTerminal(containerEl: HTMLDivElement, mods: XtermModules) {
|
||||
// xterm.js requires literal color strings in its theme config; CSS variables
|
||||
// and oklch() are not supported by the canvas renderer. These values are
|
||||
// intentionally hardcoded to match the terminal well aesthetic.
|
||||
const term = new Terminal({
|
||||
const term = new mods.Terminal({
|
||||
theme: {
|
||||
background: '#0a0a0a',
|
||||
foreground: '#d4d4d4',
|
||||
@@ -101,7 +107,7 @@ export default function BashExecModal({ isOpen, onClose, containerId, containerN
|
||||
cursorBlink: true,
|
||||
});
|
||||
|
||||
const fitAddon = new FitAddon();
|
||||
const fitAddon = new mods.FitAddon();
|
||||
term.loadAddon(fitAddon);
|
||||
term.open(containerEl);
|
||||
|
||||
@@ -200,6 +206,7 @@ export default function BashExecModal({ isOpen, onClose, containerId, containerN
|
||||
}
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
// Clean up ResizeObserver
|
||||
const el = terminalRef.current as TerminalContainer | null;
|
||||
if (el?.__resizeObserver) {
|
||||
|
||||
Reference in New Issue
Block a user