Build Optimization
Elftia's main bundle has been optimized from 1,812 KB to 759 KB (-58%). This document records optimization strategies and standards to prevent regression.
Bundle Size Limits
Hard Limits (Must Comply)
| Metric | Limit | Current | Check Method |
|---|---|---|---|
| Max single chunk | < 1 MB | 759 KB | npm run verify:build |
| Main bundle (gzipped) | < 300 KB | 221 KB | npm run verify:build |
| Vite warnings | 0 | 0 | Build output |
Recommended Targets
| Metric | Target | Current | Priority |
|---|---|---|---|
| Main bundle (uncompressed) | < 500 KB | 759 KB | P1 |
| Total bundle | < 3 MB | 6.24 MB | P2 |
| First screen load | < 2s | ~2s | P0 |
Check Commands
# Quick verification (4 key metrics)
npm run verify:build
# Detailed analysis (chunk categorization stats)
npm run analyze:build
# Visual analysis (generate stats.html)
npm run build:renderer
# Open dist/stats.html
Code Splitting Strategy
Route-Level Code Splitting (Required)
All page components must use React.lazy for lazy loading. lazy/ is a directory organized by purpose (pages.tsx / workspaces.tsx / inline.tsx / shell.tsx / skeletons.tsx / with-suspense.tsx / index.ts barrel), add new page by modifying lazy/pages.tsx:
// packages/renderer/src/app/lazy/pages.tsx
export const LazySettingsPage = lazy(() => import('../Settings'));
export const SuspenseSettings = withSuspense(LazySettingsPage, PageSkeleton);
// packages/renderer/src/app/App.tsx — old import paths still valid (barrel re-export)
import { SuspenseSettings as Settings } from './components/lazy';
<Route path="/settings" element={<Settings />} />
// Don't do this: direct import of page components
import Settings from './components/Settings';
<Route path="/settings" element={<Settings />} />
Large Component Lazy Loading (Recommended)
Use lazy loading when a single component > 100 KB or depends on large third-party libraries.
Already lazy-loaded components:
| Component | Size | Dependency |
|---|---|---|
MermaidDiagram | 451 KB | mermaid |
HtmlPreviewPanel | 198 KB | iframe sandbox |
CodeEditor | 34 KB | CodeMirror |
Shell / StandaloneShell | 9 KB | xterm |
Decision criteria:
| Condition | Lazy Load? |
|---|---|
| Component bundle > 100 KB | Yes |
| Depends on large third-party library | Yes |
| Not needed for first screen | Yes |
| Low usage frequency | Yes |
| Small common component (< 10 KB) | No |
| Core component needed for first screen | No |
| Frequently toggled UI component | No |
Context Optimization
Move non-globally-required Context to page-level components:
// Good: page-level Context
export function Settings() {
return (
<SettingsProvider>
<SettingsContent />
</SettingsProvider>
);
}
// Bad: load unnecessary Context globally
<GlobalContext>
<Routes />
</GlobalContext>
Must analyze dependencies before moving Context to ensure cross-page state sharing isn't broken.
Vendor Chunks Configuration
Current strategy groups by update frequency and usage frequency:
// vite.config.js — manualChunks
{
'vendor-react': ['react', 'react-dom', 'react-router-dom'],
'vendor-ui': ['@radix-ui/react-context-menu', '@radix-ui/react-dialog', ...],
'vendor-icons': ['lucide-react'],
'vendor-utils': ['clsx', 'tailwind-merge', 'class-variance-authority', 'zustand'],
'vendor-markdown': ['react-markdown', 'remark-gfm', 'rehype-highlight'],
'vendor-codemirror': ['@codemirror/state', '@codemirror/view', ...],
'vendor-xterm': ['@xterm/xterm', '@xterm/addon-fit', '@xterm/addon-web-links'],
}
Grouping Principles
| Type | Update Frequency | Cache Priority | Examples |
|---|---|---|---|
| Core frameworks | Low | Highest | React, React DOM |
| Large third-party libs | Low | High | CodeMirror, xterm |
| UI component library | Medium | Medium | Radix UI, lucide |
| Utility libraries | Medium | Medium | clsx, zustand |
New Dependency Check
When adding new third-party dependencies:
# 1. Check size
npm info <package> dist.unpackedSize
# 2. If > 100 KB, add to vendor chunks
# 3. If high update frequency, create separate vendor chunk
# 4. Verify tree shaking support
# 5. Run npm run analyze:build to check impact
Tree Shaking
Correct Import Methods
// Good: named imports (supports tree shaking)
import { Button, Input, Select } from '@/components/ui';
import { Home, Settings, User } from 'lucide-react';
// Good: Radix UI namespace imports (official recommendation)
import * as Dialog from '@radix-ui/react-dialog';
// Bad: import entire library
import * as UI from '@/components/ui';
import * as Icons from 'lucide-react';
Check Unused Imports
npm run typecheck # TypeScript detects unused imports
npm run lint:eslint # ESLint warns about unused variables
Vite Production Build Configuration
// vite.config.js — key configuration
build: {
minify: 'terser', // terser has better compression than esbuild
sourcemap: false, // disable sourcemap in production
chunkSizeWarningLimit: 500, // chunk size warning threshold (KB)
terserOptions: {
compress: {
drop_console: true, // remove console.log (keep warn/error)
drop_debugger: true, // remove debugger
},
},
}
Common Issues and Solutions
Main Bundle Too Large (> 500 KB)
Investigation steps:
- Run
npm run build:renderer, opendist/stats.html - Check main bundle composition, find largest modules
- Check if large components are missing lazy loading
- Check for unnecessary global imports
Solutions:
- Convert large components to lazy loading
- Remove unused imports
- Convert page components to route-level lazy loading
Vendor Chunks Too Large (> 500 KB)
Solutions:
- Split large vendors into smaller chunks
- Regroup by update frequency
- Check for duplicate dependencies
Total Bundle Too Large (> 5 MB)
Solutions:
- Lazy load chart libraries (Mermaid, Cytoscape, etc.)
- Remove infrequently used features or dependencies
- Consider using smaller alternative libraries
Code Splitting Failed
Investigation steps:
- Check lazy loading configuration for relevant split files in
lazy/directory (page-level inlazy/pages.tsx, workspace bodies inlazy/workspaces.tsx, inline chat components inlazy/inline.tsx) - Check if lazy-loaded components are used in
App.tsx - Check
manualChunksin Vite configuration
Pre-Commit Checklist
When Adding New Pages
- Page component added to
lazy/pages.tsx(or corresponding split file) - Wrapped with
withSuspenseand provided fallback - Using lazy-loaded version in
App.tsx - Run
npm run build:rendererto verify separate chunk
When Adding Large Components
- Component size > 100 KB → use lazy loading
- Depends on large third-party library → use lazy loading
- Provide appropriate loading state
When Adding Third-Party Dependencies
- Check dependency size
- Dependency > 100 KB → add to vendor chunks
- High update frequency → separate vendor chunk
- Supports tree shaking → use named imports
- Run
npm run analyze:buildto check impact
Continuous Optimization Suggestions
Before Each Release
npm run verify:build
npm run build:renderer | grep -i "warning"
# If main bundle increased > 50 KB, investigate reason
Monthly
npm run build:renderer
# Open dist/stats.html to check optimizable modules
npm outdated
npm update
Quarterly
- Review all vendor chunks configuration
- Evaluate new optimization strategies
- Evaluate whether to remove infrequently used features
- Update this standards document
CI/CD Integration
Recommend adding bundle size checks in CI:
- name: Build and verify
run: |
npm run build:renderer
npm run verify:build || echo "Warning: Bundle size check failed"
Performance Metrics Baseline
| Metric | Baseline | Target | Monitoring |
|---|---|---|---|
| Main bundle | 759 KB | < 500 KB | verify:build |
| First screen load | ~2s | < 2s | Lighthouse |
| Total bundle | 6.24 MB | < 3 MB | analyze:build |
| Chunk count | 76 | - | analyze:build |
Reference Resources
- Vite - Build Optimizations
- React.lazy - Code Splitting
- Rollup - Code Splitting
- rollup-plugin-visualizer — Bundle visualization