Skip to content

Commit c808ca2

Browse files
Copilothotlong
andcommitted
Add documentation for lazy-loaded plugins architecture
Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
1 parent 7e17d6a commit c808ca2

3 files changed

Lines changed: 423 additions & 0 deletions

File tree

docs/lazy-loaded-plugins.md

Lines changed: 231 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,231 @@
1+
# Lazy-Loaded Plugins Architecture
2+
3+
This document explains how Object UI implements lazy-loaded plugins to optimize bundle size.
4+
5+
## Overview
6+
7+
Object UI supports heavy components (like Monaco Editor and Recharts) as separate plugin packages that are lazy-loaded on demand. This keeps the main application bundle small while still providing powerful functionality.
8+
9+
## Architecture
10+
11+
### Traditional Approach (Bad ❌)
12+
```typescript
13+
// This loads Monaco Editor immediately, even if never used
14+
import Editor from '@monaco-editor/react';
15+
16+
function CodeEditor() {
17+
return <Editor {...props} />;
18+
}
19+
```
20+
21+
### Lazy Loading - Host App Responsibility (Not Ideal ⚠️)
22+
```typescript
23+
// Forces every app to implement lazy loading
24+
const CodeEditor = React.lazy(() => import('./CodeEditor'));
25+
26+
function App() {
27+
return (
28+
<Suspense fallback={<Skeleton />}>
29+
<CodeEditor />
30+
</Suspense>
31+
);
32+
}
33+
```
34+
35+
### Internal Lazy Loading (Best ✅)
36+
```typescript
37+
// The plugin handles lazy loading internally
38+
import '@object-ui/plugin-editor';
39+
40+
// Monaco is NOT loaded yet
41+
// It only loads when a code-editor component is rendered
42+
const schema = { type: 'code-editor', value: '...' };
43+
```
44+
45+
## Implementation Pattern
46+
47+
Each plugin package follows this structure:
48+
49+
### 1. Heavy Implementation File (`XxxImpl.tsx`)
50+
```typescript
51+
// packages/plugin-editor/src/MonacoImpl.tsx
52+
import Editor from '@monaco-editor/react'; // Heavy import
53+
54+
export default function MonacoImpl(props) {
55+
return <Editor {...props} />;
56+
}
57+
```
58+
59+
### 2. Lazy Wrapper (`index.tsx`)
60+
```typescript
61+
// packages/plugin-editor/src/index.tsx
62+
import React, { Suspense } from 'react';
63+
import { Skeleton } from '@object-ui/components';
64+
65+
// Lazy load the implementation
66+
const LazyMonacoEditor = React.lazy(() => import('./MonacoImpl'));
67+
68+
export const CodeEditorRenderer = (props) => (
69+
<Suspense fallback={<Skeleton className="w-full h-[400px]" />}>
70+
<LazyMonacoEditor {...props} />
71+
</Suspense>
72+
);
73+
74+
// Auto-register with ComponentRegistry
75+
ComponentRegistry.register('code-editor', CodeEditorRenderer);
76+
77+
// Export for manual integration
78+
export const editorComponents = {
79+
'code-editor': CodeEditorRenderer
80+
};
81+
```
82+
83+
### 3. Build Configuration (`vite.config.ts`)
84+
```typescript
85+
export default defineConfig({
86+
build: {
87+
lib: {
88+
entry: resolve(__dirname, 'src/index.tsx'),
89+
name: 'ObjectUIPluginEditor',
90+
},
91+
rollupOptions: {
92+
// Externalize dependencies
93+
external: ['react', 'react-dom', '@object-ui/components', '@object-ui/core'],
94+
},
95+
},
96+
});
97+
```
98+
99+
## Bundle Analysis
100+
101+
### Plugin-Editor Build Output
102+
```
103+
dist/index.js 0.19 kB │ gzip: 0.15 kB
104+
dist/MonacoImpl-DCiwKyYW.js 19.42 kB │ gzip: 5.89 kB
105+
dist/index-CpP31686.js 22.42 kB │ gzip: 6.74 kB
106+
dist/index.umd.cjs 30.37 kB │ gzip: 10.88 kB
107+
```
108+
109+
### Plugin-Charts Build Output
110+
```
111+
dist/index.js 0.19 kB │ gzip: 0.15 kB
112+
dist/index-JeMjZMU4.js 22.38 kB │ gzip: 6.69 kB
113+
dist/ChartImpl-BJBP1UnW.js 541.17 kB │ gzip: 136.04 kB
114+
dist/index.umd.cjs 393.20 kB │ gzip: 118.97 kB
115+
```
116+
117+
### Playground Build Output
118+
When the playground imports both plugins, the chunks are preserved:
119+
```
120+
dist/assets/MonacoImpl-DCiwKyYW-D65z0X-D.js 15.26 kB │ gzip: 5.25 kB
121+
dist/assets/ChartImpl-BJBP1UnW-DO38vX_d.js 348.10 kB │ gzip: 104.54 kB
122+
dist/assets/index-CyDHUpwF.js 2,212.33 kB │ gzip: 561.16 kB
123+
```
124+
125+
Notice that:
126+
- `MonacoImpl` and `ChartImpl` are separate chunks
127+
- They are NOT included in the main `index.js` bundle
128+
- They will only be fetched when the components are rendered
129+
130+
## Creating New Lazy-Loaded Plugins
131+
132+
1. **Create the package structure**:
133+
```bash
134+
mkdir -p packages/plugin-yourfeature/src
135+
```
136+
137+
2. **Create the heavy implementation** (`HeavyImpl.tsx`):
138+
```typescript
139+
import HeavyLibrary from 'heavy-library';
140+
141+
export default function HeavyImpl(props) {
142+
return <HeavyLibrary {...props} />;
143+
}
144+
```
145+
146+
3. **Create the lazy wrapper** (`index.tsx`):
147+
```typescript
148+
import React, { Suspense } from 'react';
149+
import { ComponentRegistry } from '@object-ui/core';
150+
import { Skeleton } from '@object-ui/components';
151+
152+
const LazyComponent = React.lazy(() => import('./HeavyImpl'));
153+
154+
export const YourRenderer = (props) => (
155+
<Suspense fallback={<Skeleton />}>
156+
<LazyComponent {...props} />
157+
</Suspense>
158+
);
159+
160+
ComponentRegistry.register('your-component', YourRenderer);
161+
162+
export const yourComponents = {
163+
'your-component': YourRenderer
164+
};
165+
```
166+
167+
4. **Configure build** (`vite.config.ts`):
168+
```typescript
169+
import { defineConfig } from 'vite';
170+
import react from '@vitejs/plugin-react';
171+
172+
export default defineConfig({
173+
plugins: [react()],
174+
build: {
175+
lib: {
176+
entry: resolve(__dirname, 'src/index.tsx'),
177+
name: 'ObjectUIPluginYourFeature',
178+
},
179+
rollupOptions: {
180+
external: ['react', 'react-dom', '@object-ui/components', '@object-ui/core'],
181+
},
182+
},
183+
});
184+
```
185+
186+
5. **Use in the app**:
187+
```typescript
188+
// app/src/App.tsx
189+
import '@object-ui/plugin-yourfeature';
190+
191+
// Now use it in schemas
192+
const schema = { type: 'your-component', ... };
193+
```
194+
195+
## Benefits
196+
197+
1. **Smaller Initial Bundle**: Heavy libraries are not included in the main bundle
198+
2. **Faster Page Loads**: Initial page load only includes essential code
199+
3. **Better UX**: Components show loading skeletons while chunks download
200+
4. **Zero Configuration for Users**: The plugin handles all lazy loading internally
201+
5. **Automatic Code Splitting**: Vite automatically splits the code at build time
202+
203+
## Verification
204+
205+
You can verify lazy loading works by:
206+
207+
1. **Build the playground**:
208+
```bash
209+
cd apps/playground
210+
pnpm build
211+
ls -lh dist/assets/ | grep -E "(Monaco|Chart)"
212+
```
213+
214+
2. **Check for separate chunks**:
215+
```
216+
MonacoImpl-xxx.js (~15-20 KB)
217+
ChartImpl-xxx.js (~350-540 KB)
218+
```
219+
220+
3. **Test in browser**:
221+
- Open DevTools → Network tab
222+
- Load a page WITHOUT the plugin components
223+
- The Monaco/Chart chunks should NOT be loaded
224+
- Navigate to a page WITH the plugin components
225+
- The chunks should NOW be loaded
226+
227+
## References
228+
229+
- React.lazy() documentation: https://react.dev/reference/react/lazy
230+
- Vite code splitting: https://vitejs.dev/guide/features.html#code-splitting
231+
- Rollup chunking: https://rollupjs.org/configuration-options/#output-manualchunks

packages/plugin-charts/README.md

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
# Plugin Charts - Lazy-Loaded Recharts Components
2+
3+
A lazy-loaded charting component for Object UI based on Recharts.
4+
5+
## Features
6+
7+
- **Internal Lazy Loading**: Recharts is loaded on-demand using `React.lazy()` and `Suspense`
8+
- **Zero Configuration**: Just import the package and use `type: 'chart-bar'` in your schema
9+
- **Automatic Registration**: Components auto-register with the ComponentRegistry
10+
- **Skeleton Loading**: Shows a skeleton while Recharts loads
11+
12+
## Installation
13+
14+
```bash
15+
pnpm add @object-ui/plugin-charts
16+
```
17+
18+
## Usage
19+
20+
### Automatic Registration (Side-Effect Import)
21+
22+
```typescript
23+
// In your app entry point (e.g., App.tsx or main.tsx)
24+
import '@object-ui/plugin-charts';
25+
26+
// Now you can use chart-bar type in your schemas
27+
const schema = {
28+
type: 'chart-bar',
29+
data: [
30+
{ name: 'Jan', value: 400 },
31+
{ name: 'Feb', value: 300 },
32+
{ name: 'Mar', value: 600 }
33+
],
34+
dataKey: 'value',
35+
xAxisKey: 'name',
36+
height: 400
37+
};
38+
```
39+
40+
### Manual Integration
41+
42+
```typescript
43+
import { chartComponents } from '@object-ui/plugin-charts';
44+
import { ComponentRegistry } from '@object-ui/core';
45+
46+
// Manually register if needed
47+
Object.entries(chartComponents).forEach(([type, component]) => {
48+
ComponentRegistry.register(type, component);
49+
});
50+
```
51+
52+
## Schema API
53+
54+
```typescript
55+
{
56+
type: 'chart-bar',
57+
data?: Array<Record<string, any>>, // Chart data
58+
dataKey?: string, // Y-axis data key (default: 'value')
59+
xAxisKey?: string, // X-axis label key (default: 'name')
60+
height?: number, // Chart height in pixels (default: 400)
61+
color?: string, // Bar color (default: '#8884d8')
62+
className?: string // Tailwind classes
63+
}
64+
```
65+
66+
## Lazy Loading Architecture
67+
68+
The plugin uses a two-file pattern for optimal code splitting:
69+
70+
1. **`ChartImpl.tsx`**: Contains the actual Recharts import (heavy ~541 KB)
71+
2. **`index.tsx`**: Entry point with `React.lazy()` wrapper (light)
72+
73+
When bundled, Vite automatically creates separate chunks:
74+
- `index.js` (~200 bytes) - The entry point
75+
- `ChartImpl-xxx.js` (~541 KB minified, ~136 KB gzipped) - The lazy-loaded implementation
76+
77+
The Recharts library is only downloaded when a `chart-bar` component is actually rendered, not on initial page load.
78+
79+
## Build Output Example
80+
81+
```
82+
dist/index.js 0.19 kB # Entry point
83+
dist/ChartImpl-BJBP1UnW.js 541.17 kB # Lazy chunk (loaded on demand)
84+
dist/index.umd.cjs 393.20 kB # UMD bundle
85+
```
86+
87+
## Development
88+
89+
```bash
90+
# Build the plugin
91+
pnpm build
92+
93+
# The package will generate proper ESM and UMD builds with lazy loading preserved
94+
```
95+
96+
## Bundle Size Impact
97+
98+
By using lazy loading, the main application bundle stays lean:
99+
- Without lazy loading: +541 KB on initial load
100+
- With lazy loading: +0.19 KB on initial load, +541 KB only when chart is rendered
101+
102+
This results in significantly faster initial page loads for applications that don't use charts on every page.

0 commit comments

Comments
 (0)