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();