Skip to content

Commit b59155d

Browse files
committed
perf(editor): split the bundle so the shell loads first
1 parent 5e4bc3b commit b59155d

8 files changed

Lines changed: 1647 additions & 1593 deletions

File tree

resources/js/app.tsx

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { StrictMode } from 'react';
1+
import { StrictMode, useEffect, useState } from 'react';
22
import { createRoot } from 'react-dom/client';
33
import { DemoWorkspace } from './components/DemoWorkspace';
44
import { ToastProvider } from './components/Toasts';
@@ -12,13 +12,37 @@ if (!rootElement) {
1212
throw new Error('React root element was not found.');
1313
}
1414

15-
const realtimeReady = startRealtime(readRealtimeSettings(rootElement));
15+
const settings = readRealtimeSettings(rootElement);
1616
const sessionId = rootElement.dataset.sessionId;
1717

18+
/**
19+
* Brings the editor up straight away and connects live updates behind it, so
20+
* loading Echo never delays the first paint.
21+
*/
22+
function Editor({ sessionId }: { sessionId: string }) {
23+
const [realtimeReady, setRealtimeReady] = useState(false);
24+
25+
useEffect(() => {
26+
let cancelled = false;
27+
28+
void startRealtime(settings).then(ready => {
29+
if (!cancelled) {
30+
setRealtimeReady(ready);
31+
}
32+
});
33+
34+
return () => {
35+
cancelled = true;
36+
};
37+
}, []);
38+
39+
return <EditorApp sessionId={sessionId} realtimeReady={realtimeReady} />;
40+
}
41+
1842
createRoot(rootElement).render(
1943
<StrictMode>
2044
{sessionId ? (
21-
<EditorApp sessionId={sessionId} realtimeReady={realtimeReady} />
45+
<Editor sessionId={sessionId} />
2246
) : (
2347
<ToastProvider>
2448
<EditorStoreProvider>

resources/js/components/EditorWorkspace.tsx

Lines changed: 18 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
1-
import { useCallback, useEffect, useState } from 'react';
1+
import { lazy, Suspense, useCallback, useEffect, useState } from 'react';
22
import { ApiError } from '../api/client';
33
import { deleteMenu, fetchMenu, fetchMenus, menuKey, saveMenu, SETTINGS_DESCRIPTOR } from '../editor/menus';
44
import { EDITOR_VERSION } from '../editor/config';
55
import { activeTab, hasUnsavedWork, useEditorStore } from '../editor/store';
66
import { validateMenuSource, type MenuDiagnostics } from '../editor/validator';
77
import { AdminTerminal } from './AdminTerminal';
88
import { MaintenanceBanner } from './MaintenanceBanner';
9-
import { RealtimeBridge } from './RealtimeBridge';
9+
10+
// Echo and Pusher ride along with this component, so it is fetched only once
11+
// a session actually has live updates available.
12+
const RealtimeBridge = lazy(() => import('./RealtimeBridge').then(m => ({ default: m.RealtimeBridge })));
1013
import { WorkspaceLayout } from './WorkspaceLayout';
1114
import { useToast } from './Toasts';
1215
import type { MenuDescriptor, SessionState } from '../types/editor';
@@ -144,17 +147,19 @@ export function EditorWorkspace({ sessionId, session, realtimeReady }: EditorWor
144147
return (
145148
<>
146149
{realtimeReady && (
147-
<RealtimeBridge
148-
sessionId={sessionId}
149-
onPluginStatus={online => {
150-
dispatch({ type: 'plugin/status', online });
151-
152-
if (online) {
153-
void loadMenus();
154-
}
155-
}}
156-
onMenuChanged={() => void loadMenus()}
157-
/>
150+
<Suspense fallback={null}>
151+
<RealtimeBridge
152+
sessionId={sessionId}
153+
onPluginStatus={online => {
154+
dispatch({ type: 'plugin/status', online });
155+
156+
if (online) {
157+
void loadMenus();
158+
}
159+
}}
160+
onMenuChanged={() => void loadMenus()}
161+
/>
162+
</Suspense>
158163
)}
159164

160165
<WorkspaceLayout

resources/js/components/WorkspaceLayout.tsx

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,16 @@
1-
import type { ReactNode } from 'react';
1+
import { lazy, Suspense, type ReactNode } from 'react';
22
import { activeTab, isDirty, useEditorStore } from '../editor/store';
33
import { EditorTabs } from './EditorTabs';
44
import { MenuSidebar } from './MenuSidebar';
55
import { OutputPanel } from './OutputPanel';
66
import { StatusBar } from './StatusBar';
7-
import { VisualEditor } from './visual/VisualEditor';
8-
import { FormBuilder } from './visual/FormBuilder';
9-
import { ConfigEditor } from './visual/ConfigEditor';
10-
import { YamlEditor } from './YamlEditor';
7+
8+
// Each of these pulls a heavy dependency (CodeMirror, the canvas, the form
9+
// builder), and only one of them is on screen at a time.
10+
const YamlEditor = lazy(() => import('./YamlEditor').then(m => ({ default: m.YamlEditor })));
11+
const VisualEditor = lazy(() => import('./visual/VisualEditor').then(m => ({ default: m.VisualEditor })));
12+
const FormBuilder = lazy(() => import('./visual/FormBuilder').then(m => ({ default: m.FormBuilder })));
13+
const ConfigEditor = lazy(() => import('./visual/ConfigEditor').then(m => ({ default: m.ConfigEditor })));
1114
import type { MenuDiagnostics } from '../editor/validator';
1215
import type { MenuDescriptor } from '../types/editor';
1316

@@ -137,6 +140,7 @@ export function WorkspaceLayout({
137140

138141
<div className="editor-content">
139142
<div className="editor-body">
143+
<Suspense fallback={<div className="item-editor-content">Loading the editor...</div>}>
140144
{current === null ? (
141145
<div className="welcome-screen">
142146
<div className="welcome-icon">📝</div>
@@ -168,6 +172,7 @@ export function WorkspaceLayout({
168172
onChange={content => dispatch({ type: 'tab/edited', key: current.key, content })}
169173
/>
170174
)}
175+
</Suspense>
171176
</div>
172177

173178
<OutputPanel diagnostics={diagnostics} onClose={onCloseDiagnostics} />

resources/js/components/visual/ItemPalette.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { useDeferredValue, useMemo, useState } from 'react';
2-
import { formatMaterialName, getAvailableMaterials } from '../../editor/materials';
2+
import { formatMaterialName } from '../../editor/materials';
3+
import { getAvailableMaterials } from '../../editor/materialCatalogue';
34
import { ItemIcon } from './ItemIcon';
45

56
interface ItemPaletteProps {

resources/js/components/visual/MaterialSelector.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { useDeferredValue, useMemo, useState } from 'react';
2-
import { formatMaterialName, getAvailableMaterials } from '../../editor/materials';
2+
import { formatMaterialName } from '../../editor/materials';
3+
import { getAvailableMaterials } from '../../editor/materialCatalogue';
34
import { Modal } from '../Modal';
45
import { ItemIcon } from './ItemIcon';
56

0 commit comments

Comments
 (0)