mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-08-21 23:56:39 +00:00
perf(frontend): lazy-load Monaco editor + diff editor (#824)
Monaco-editor and @monaco-editor/react were imported eagerly from
main.tsx so that the locally-bundled Monaco was registered with
@monaco-editor/react (CSP blocks the default CDN load). This pulled
the ~3 MB monaco chunk into every cold app start regardless of
whether a user ever opened the editor.
Move the Monaco setup into a new frontend/src/lib/monacoLoader.tsx
module that exports React.lazy-wrapped Editor and DiffEditor
components. The lazy factory awaits a one-shot setupMonaco() that
dynamic-imports monaco-editor, @monaco-editor/react, and the editor
worker, then calls loader.config({ monaco }) and sets
window.MonacoEnvironment before resolving the underlying component.
Concurrent first mounts share a single setup promise so the work
runs at most once per process. The three consumers (EditorLayout,
FileViewer, GitSourceDiffDialog) wrap their editor in <Suspense>
with a transparent fallback that preserves layout while the chunk
loads.
main.tsx loses three eager imports plus the MonacoEnvironment +
loader.config bootstrap. The vite.config.ts manualChunks group from
PR #823 was already prepared for this; the monaco chunk now loads
on demand instead of being bundled into the entry chunk.
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { useState, useEffect, useMemo } from 'react';
|
||||
import Editor from '@monaco-editor/react';
|
||||
import { useState, useEffect, useMemo, Suspense } from 'react';
|
||||
import { Editor } from '@/lib/monacoLoader';
|
||||
import { AlertCircle, FileIcon, Download, Lock, Loader2, Save } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Skeleton } from '@/components/ui/skeleton';
|
||||
@@ -276,16 +276,18 @@ export function FileViewer({
|
||||
</div>
|
||||
|
||||
<div className="flex-1 min-h-0">
|
||||
<Editor
|
||||
height="100%"
|
||||
language={language}
|
||||
value={content}
|
||||
onChange={(val) => {
|
||||
if (!readOnly) setContent(val ?? '');
|
||||
}}
|
||||
theme={isDarkMode ? 'vs-dark' : 'light'}
|
||||
options={editorOptions}
|
||||
/>
|
||||
<Suspense fallback={<div className="w-full h-full" aria-busy="true" />}>
|
||||
<Editor
|
||||
height="100%"
|
||||
language={language}
|
||||
value={content}
|
||||
onChange={(val) => {
|
||||
if (!readOnly) setContent(val ?? '');
|
||||
}}
|
||||
theme={isDarkMode ? 'vs-dark' : 'light'}
|
||||
options={editorOptions}
|
||||
/>
|
||||
</Suspense>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -9,8 +9,12 @@ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||
import { render, screen, waitFor } from '@testing-library/react';
|
||||
import type { FileContentResult } from '@/lib/stackFilesApi';
|
||||
|
||||
vi.mock('@monaco-editor/react', () => ({
|
||||
default: () => <div data-testid="monaco-editor" />,
|
||||
// FileViewer now imports `Editor` from the lazy loader, not directly from
|
||||
// @monaco-editor/react. Mock the loader so tests skip Monaco's setup path
|
||||
// and the editor renders synchronously.
|
||||
vi.mock('@/lib/monacoLoader', () => ({
|
||||
Editor: () => <div data-testid="monaco-editor" />,
|
||||
DiffEditor: () => <div data-testid="monaco-diff-editor" />,
|
||||
}));
|
||||
|
||||
vi.mock('@/lib/stackFilesApi', () => ({
|
||||
|
||||
Reference in New Issue
Block a user