This document details the architecture for lazy-loading heavy components (like code editors or charts) in Object UI to optimize the application bundle size.
Object UI encapsulates heavy dependencies into separate plugin packages. The architecture ensures that these dependencies are only downloaded by the browser when the specific component is actually rendered on the screen.
Directly importing heavy libraries causes them to be included in the main bundle, slowing down the initial load for all users, even those who never use the component.
// The heavy library is bundled immediately
import Editor from '@monaco-editor/react';
function CodeEditor() {
return <Editor {...props} />;
}Forcing the application developer to handle lazy loading leaks implementation details and creates repetitive boilerplate code.
// Forces every consumer to implement Suspense logic manually
const CodeEditor = React.lazy(() => import('./CodeEditor'));
function App() {
return (
<Suspense fallback={<Skeleton />}>
<CodeEditor />
</Suspense>
);
}The plugin package handles lazy loading internally. Consumers import it normally, but the browser only fetches the heavy code when needed.
// Import the plugin to register it
import '@object-ui/plugin-editor';
// The heavy specific code is NOT loaded yet.
// It will be fetched automatically ONLY when this schema is rendered:
const schema = { type: 'code-editor', value: '...' };Every plugin package follows a strict separation of concerns to ensure code splitting works correctly.
This file contains the actual heavy dependencies.
// packages/plugin-editor/src/MonacoImpl.tsx
// 🔴 Heavy dependencies are isolated here.
// This file becomes a separate chunk during build.
import Editor from '@monaco-editor/react';
export default function MonacoImpl(props) {
return <Editor {...props} />;
}The entry point uses React.lazy and Suspense to wrap the implementation. This is the only file that gets included in the initial bundle.
// packages/plugin-editor/src/index.tsx
import React, { Suspense } from 'react';
import { ComponentRegistry } from '@object-ui/core';
import { Skeleton } from '@object-ui/components';
// 🟢 Lazy load the implementation file
const LazyMonacoEditor = React.lazy(() => import('./MonacoImpl'));
export const CodeEditorRenderer = (props) => (
<Suspense fallback={<Skeleton className="w-full h-[400px]" />}>
<LazyMonacoEditor {...props} />
</Suspense>
);
// Register directly with the core engine
ComponentRegistry.register('code-editor', CodeEditorRenderer);
// Export for manual integration if needed
export const editorComponents = {
'code-editor': CodeEditorRenderer
};Types are owned by the plugin to maintain decoupling.
// packages/plugin-editor/src/types.ts
import type { BaseSchema } from '@object-ui/types';
/**
* Code Editor component schema.
* Defined locally to avoid polluting the core package.
*/
export interface CodeEditorSchema extends BaseSchema {
type: 'code-editor';
value?: string;
language?: string;
theme?: 'vs-dark' | 'light';
// ... specific props
}Configure Rollup/Vite to correctly bundle the library while externalizing core dependencies.
export default defineConfig({
build: {
lib: {
entry: resolve(__dirname, 'src/index.tsx'),
name: 'ObjectUIPluginEditor',
},
rollupOptions: {
// Ensure core libraries and React are not bundled into the plugin
external: ['react', 'react-dom', '@object-ui/components', '@object-ui/core'],
},
},
});Each plugin package is responsible for exporting its own interfaces. This allows plugins to evolve independently of the core framework.
import type { CodeEditorSchema } from '@object-ui/plugin-editor';Defining plugin types in @object-ui/types creates tight coupling and forces core updates for every plugin change.
The build output demonstrates the separation:
dist/index.js: Lightweight wrapper (~1-2kb).dist/MonacoImpl-xxxx.js: Heavy chunk (only loaded on demand).
When an application uses the plugin, Vite's bundler respects this split, preserving the heavy chunk as a separate file in the final dist/assets folder.
- Structure: Create
packages/plugin-yourfeature. - Isolate: Put heavy code in a default exported file (e.g.,
HeavyImpl.tsx). - Wrap: Create a wrapper in
index.tsxusingReact.lazy(() => import('./HeavyImpl')). - Register: Call
ComponentRegistry.registerin the wrapper. - Build: Set up
vite.config.tsto externalize@object-ui/coreand@object-ui/components.
To verify lazy loading works in your application:
- Run
pnpm buildin your app. - Inspect
dist/assets. You should see separate files for the plugin implementations (e.g.,MonacoImpl-....js). - Open the app in a browser with the Network tab open.
- Navigate to a page without the plugin component. The heavy chunk should not load.
- Navigate to a page with the component. The heavy chunk should load immediately.