From 0e5a53af1d6cadeed876df813613726913cd6b39 Mon Sep 17 00:00:00 2001 From: Matias Palma Date: Tue, 21 Apr 2026 00:58:27 -0400 Subject: [PATCH] improvements: code-split Monaco, Marketplace, AI chat, terminal, and Settings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The desktop shell was pulling every heavy surface into the initial JavaScript bundle even though most of them are only reachable after the user has made a deliberate choice (open a file, open the AI panel, open the terminal, open the marketplace, open Settings). Route heavy modules through `next/dynamic` and dynamic `import()` so they ship as their own chunks and arrive only when needed: - `page.tsx` — `EditorArea`, `BottomPanel`, `RightPanelSlot`, and `SettingsView` are now dynamic imports (`ssr: false` because this app is always client-rendered inside Tauri and several components touch `window` at module scope). - `EditorArea.tsx` — `MonacoEditor` and `MarketplaceView` are now dynamic imports. Monaco's loader config runs inside the lazy factory so the `/vs` path is still wired up before first mount. - `PluginManager.ts` — built-in addons (AI assistant, Git explorer, five language addons) switched from top-level `import * as` to `await import()` inside `bootstrap()`. AiChat (which pulls react-markdown + remark-gfm) and GitExplorer (picomatch + git dialogs) are the biggest wins; language addons are kicked off in parallel so one slow chunk doesn't block the rest. No behavior change for end users — same panels, same interactions, just smaller first-paint cost. `next build` still produces a clean static page. Closes #81 --- apps/desktop/src/api/PluginManager.ts | 48 +++++++++++++--------- apps/desktop/src/app/page.tsx | 15 +++++-- apps/desktop/src/components/EditorArea.tsx | 28 +++++++++---- 3 files changed, 59 insertions(+), 32 deletions(-) diff --git a/apps/desktop/src/api/PluginManager.ts b/apps/desktop/src/api/PluginManager.ts index 49f24e1e..c52ac92b 100644 --- a/apps/desktop/src/api/PluginManager.ts +++ b/apps/desktop/src/api/PluginManager.ts @@ -1,10 +1,3 @@ -import * as builtinAiAssistant from "@/addons/builtin.ai-assistant/index"; -import * as builtinGitExplorer from "@/addons/builtin.git-explorer/index"; -import * as builtinLanguageTypescript from "@/addons/builtin.language.typescript/index"; -import * as builtinLanguagePython from "@/addons/builtin.language.python/index"; -import * as builtinLanguageRust from "@/addons/builtin.language.rust/index"; -import * as builtinLanguageHtml from "@/addons/builtin.language.html/index"; -import * as builtinLanguageMarkdown from "@/addons/builtin.language.markdown/index"; import { registerBuiltinTranslations } from "./builtin.l10n"; import { logger } from "@/lib/logger"; @@ -26,32 +19,47 @@ export class PluginManager { // Load translations first registerBuiltinTranslations(); + // Built-in addons are imported dynamically so they become their own + // chunks instead of riding in the initial bundle. AiChatComponent in + // particular pulls react-markdown + remark-gfm and GitExplorer pulls + // picomatch + git dialogs, so defer both until the user is past the + // first paint. try { - builtinAiAssistant.activate(); + const mod = await import("@/addons/builtin.ai-assistant/index"); + mod.activate(); logger.debug("[PluginManager] builtin.ai-assistant activated."); } catch (e) { logger.error("Failed to activate AI assistant", e); } try { - builtinGitExplorer.activate(); + const mod = await import("@/addons/builtin.git-explorer/index"); + mod.activate(); logger.debug("[PluginManager] builtin.git-explorer activated."); } catch (e) { logger.error("Failed to activate Git Explorer", e); } - // Language Addons - try { - builtinLanguageTypescript.activate(window.trixty); - builtinLanguagePython.activate(window.trixty); - builtinLanguageRust.activate(window.trixty); - builtinLanguageHtml.activate(window.trixty); - builtinLanguageMarkdown.activate(window.trixty); + // Language Addons — registered in parallel so one slow import doesn't + // block the rest of the bootstrap chain. + try { + const [ts, py, rs, html, md] = await Promise.all([ + import("@/addons/builtin.language.typescript/index"), + import("@/addons/builtin.language.python/index"), + import("@/addons/builtin.language.rust/index"), + import("@/addons/builtin.language.html/index"), + import("@/addons/builtin.language.markdown/index"), + ]); + ts.activate(window.trixty); + py.activate(window.trixty); + rs.activate(window.trixty); + html.activate(window.trixty); + md.activate(window.trixty); - logger.debug("[PluginManager] Built-in language addons activated."); - } catch (e) { - logger.error("Failed to activate language addons", e); - } + logger.debug("[PluginManager] Built-in language addons activated."); + } catch (e) { + logger.error("Failed to activate language addons", e); + } // Dynamically load external scripts from Tauri File System try { diff --git a/apps/desktop/src/app/page.tsx b/apps/desktop/src/app/page.tsx index e30d0eed..5c33fe3a 100644 --- a/apps/desktop/src/app/page.tsx +++ b/apps/desktop/src/app/page.tsx @@ -1,20 +1,27 @@ "use client"; import React, { useEffect } from "react"; +import dynamic from "next/dynamic"; import ActivityBar from "@/components/ActivityBar"; import LeftSidebarSlot from "@/components/slots/LeftSidebarSlot"; -import EditorArea from "@/components/EditorArea"; import WelcomeScreen from "@/components/WelcomeScreen"; -import RightPanelSlot from "@/components/slots/RightPanelSlot"; -import BottomPanel from "@/components/BottomPanel"; import TitleBar from "@/components/TitleBar"; -import SettingsView from "@/components/SettingsView"; import UpdaterDialog from "@/components/UpdaterDialog"; import OnboardingWizard from "@/components/OnboardingWizard"; import { ErrorBoundary } from "@/components/ErrorBoundary"; import { useApp } from "@/context/AppContext"; import { PluginManager } from "@/api/PluginManager"; +// Code-split heavy panels so Monaco, xterm, Marketplace, AI chat, and the +// Settings modal aren't downloaded until the user actually opens them. +// `ssr: false` skips the RSC attempt — this app is always client-rendered +// inside Tauri, and several of these components touch `window` at module +// scope. +const EditorArea = dynamic(() => import("@/components/EditorArea"), { ssr: false }); +const BottomPanel = dynamic(() => import("@/components/BottomPanel"), { ssr: false }); +const RightPanelSlot = dynamic(() => import("@/components/slots/RightPanelSlot"), { ssr: false }); +const SettingsView = dynamic(() => import("@/components/SettingsView"), { ssr: false }); + export default function Home() { const { isRightPanelOpen, diff --git a/apps/desktop/src/components/EditorArea.tsx b/apps/desktop/src/components/EditorArea.tsx index dd26522f..b2ab2e87 100644 --- a/apps/desktop/src/components/EditorArea.tsx +++ b/apps/desktop/src/components/EditorArea.tsx @@ -1,19 +1,31 @@ "use client"; import React, { useRef } from "react"; -import MonacoEditor, { OnMount, Monaco, loader } from "@monaco-editor/react"; -import { editor } from "monaco-editor"; - -// Configure Monaco loader to use local assets -if (typeof window !== "undefined") { - loader.config({ paths: { vs: "/vs" } }); -} +import dynamic from "next/dynamic"; +import type { OnMount, Monaco } from "@monaco-editor/react"; +import type { editor } from "monaco-editor"; import { useApp } from "@/context/AppContext"; import TabBar from "./TabBar"; import { useL10n } from "@/hooks/useL10n"; -import MarketplaceView from "./MarketplaceView"; import { ErrorBoundary } from "./ErrorBoundary"; +// Monaco is ~1.5 MB of gzipped JS and pulls language workers on top of that. +// Loading it with `next/dynamic` keeps it off the boot path; it only arrives +// once the user opens their first real file. The loader also needs `window`, +// so keep `ssr: false`. +const MonacoEditor = dynamic( + async () => { + const mod = await import("@monaco-editor/react"); + mod.loader.config({ paths: { vs: "/vs" } }); + return mod.default; + }, + { ssr: false }, +); + +// Marketplace is only reachable via the virtual `extensions` tab — no reason +// to ship it in the initial bundle next to Monaco. +const MarketplaceView = dynamic(() => import("./MarketplaceView"), { ssr: false }); + const EditorArea: React.FC = () => { const { currentFile, updateFileContent, openFiles, editorSettings } = useApp(); const { t } = useL10n();