Skip to main content

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)

MetricLimitCurrentCheck Method
Max single chunk< 1 MB759 KBnpm run verify:build
Main bundle (gzipped)< 300 KB221 KBnpm run verify:build
Vite warnings00Build output
MetricTargetCurrentPriority
Main bundle (uncompressed)< 500 KB759 KBP1
Total bundle< 3 MB6.24 MBP2
First screen load< 2s~2sP0

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 />} />

Use lazy loading when a single component > 100 KB or depends on large third-party libraries.

Already lazy-loaded components:

ComponentSizeDependency
MermaidDiagram451 KBmermaid
HtmlPreviewPanel198 KBiframe sandbox
CodeEditor34 KBCodeMirror
Shell / StandaloneShell9 KBxterm

Decision criteria:

ConditionLazy Load?
Component bundle > 100 KBYes
Depends on large third-party libraryYes
Not needed for first screenYes
Low usage frequencyYes
Small common component (< 10 KB)No
Core component needed for first screenNo
Frequently toggled UI componentNo

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>
warning

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

TypeUpdate FrequencyCache PriorityExamples
Core frameworksLowHighestReact, React DOM
Large third-party libsLowHighCodeMirror, xterm
UI component libraryMediumMediumRadix UI, lucide
Utility librariesMediumMediumclsx, 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:

  1. Run npm run build:renderer, open dist/stats.html
  2. Check main bundle composition, find largest modules
  3. Check if large components are missing lazy loading
  4. 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:

  1. Check lazy loading configuration for relevant split files in lazy/ directory (page-level in lazy/pages.tsx, workspace bodies in lazy/workspaces.tsx, inline chat components in lazy/inline.tsx)
  2. Check if lazy-loaded components are used in App.tsx
  3. Check manualChunks in Vite configuration

Pre-Commit Checklist

When Adding New Pages

  • Page component added to lazy/pages.tsx (or corresponding split file)
  • Wrapped with withSuspense and provided fallback
  • Using lazy-loaded version in App.tsx
  • Run npm run build:renderer to 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:build to 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

MetricBaselineTargetMonitoring
Main bundle759 KB< 500 KBverify:build
First screen load~2s< 2sLighthouse
Total bundle6.24 MB< 3 MBanalyze:build
Chunk count76-analyze:build

Reference Resources