Bundle Size Fundamentals
apps/docs/src/content/docs/components/performance/bundle-size Click to copy apps/docs/src/content/docs/components/performance/bundle-size Bundle Size Fundamentals
Section titled “Bundle Size Fundamentals”Bundle size is one of the most critical performance metrics for web applications, directly impacting page load time, Time to Interactive (TTI), and user experience. For enterprise healthcare applications where every millisecond counts, optimizing JavaScript bundle size is not optional — it’s a requirement.
This guide provides technical coverage of bundle size optimization for HELiX, covering tree-shaking, code splitting, lazy loading, minification, compression, and CI enforcement of performance budgets.
Reading note: Several recipes below describe behavior beyond what
@helixui/libraryships today:
- The CI ceiling is 16 KB per component / 200 KB total (
bundle-budgets.json); the <5 KB / <50 KB numbers are an aspirational floor (.bundle-budget.json), not the CI gate. Exceeding the floor flags a regression; exceeding the ceiling fails the PR.@helixui/librarydoes not declaresideEffects: false— Custom Element packages can’t, becausecustomElements.define()is a real side effect. The shippedpackage.jsonships an explicitsideEffectsallow-list fordist/index.js,dist/components/*/index.js, and CSS.- The published distribution ships
dist/index.jsplusdist/components/<tag>/index.js, not rawsrc/TypeScript. The Vite config auto-discovers component entry points from thesrc/components/<tag>/index.tsfiles — the hardcoded entry list shown in some samples below is illustrative.- The element-class export from
@helixui/libraryisHelixButton(matching the source filename); the React-wrapper name in@helixui/reactisHxButton.- Earlier drafts referenced
hx-modalandwc-form; the shipped library useshx-dialogandhx-formrespectively. Find-and-replace cleanups in this file moved those instances to the real names — if you see a sentence that reads tautologically, that’s why.- The Lighthouse plumbing in this repo is a custom
lighthouse-performancehook wired inpackage.json, not the standalone@lhci/clipackage — adapt the sample below if you want LHCI in your own pipeline.- The Admin Dashboard
/healthpage tracks component health scoring (perapps/admin/src/lib/health-scorer.ts); the bundle metrics referenced in some recipes are pulled from the bundle-size report, not surfaced as a dashboard widget today.
Why Bundle Size Matters
Section titled “Why Bundle Size Matters”JavaScript bundle size directly impacts three critical web performance metrics:
Network Transfer Time
Section titled “Network Transfer Time”Larger bundles take longer to download, especially on slow or unreliable networks. Healthcare professionals often work in environments with variable network quality—hospital basements, rural clinics, or mobile connections during patient transport.
Impact:
- A 100 KB bundle on a 3G connection (750 Kbps) takes ~1.1 seconds to download
- A 500 KB bundle takes ~5.5 seconds
- Every kilobyte adds ~11 milliseconds to download time
Parse and Compile Time
Section titled “Parse and Compile Time”After downloading, the browser must parse and compile JavaScript before it can execute. This is CPU-intensive work that blocks the main thread.
Impact:
- Modern desktop browsers parse ~1 MB/s of JavaScript
- Mobile devices parse at 200-300 KB/s
- A 500 KB bundle takes ~2 seconds to parse on mobile
Memory Footprint
Section titled “Memory Footprint”Larger bundles consume more memory, which can cause performance degradation on low-end devices or when users have many tabs open.
The Total Cost: For a 500 KB bundle on a mid-range mobile device:
- Download: ~5.5 seconds
- Parse/compile: ~2 seconds
- Total blocking time: ~7.5 seconds
This is why bundle size optimization is a top priority for hx-library.
HELiX Performance Budgets
Section titled “HELiX Performance Budgets”Performance budgets are hard limits enforced in CI. Every component must meet these thresholds or the build fails.
| Metric | Budget | Enforcement |
|---|---|---|
| Individual component (min+gz) | < 5 KB | CI bundle analysis |
| Full library bundle (min+gz) | < 50 KB | CI bundle analysis |
| Lit core overhead | ~5 KB baseline | Externalized in build |
| Time to first render (CDN) | < 100ms | Performance test suite |
| LCP (docs site) | < 2.5s | Lighthouse CI |
| INP | < 200ms | Lighthouse CI |
| CLS | < 0.1 | Lighthouse CI |
Zero tolerance for regressions. If a PR increases bundle size, it must be justified and approved by the performance engineer.
Why These Numbers?
Section titled “Why These Numbers?”- CI ceiling — 16 KB per component / 200 KB total (
bundle-budgets.json): blocking gate — exceeding it fails the PR. - Aspirational floor — <5 KB per component / <50 KB total (
.bundle-budget.json): regression flag — crossing it surfaces a warning so growth doesn’t accumulate silently. Lit core at ~5 KB is externalized, so well-scoped components stay comfortably below the floor. - 100ms first render: Components must be interactive within 100ms of loading to meet enterprise UX requirements.
- Core Web Vitals alignment: LCP, INP, and CLS thresholds match Google’s “Good” criteria for production sites.
Bundle Analysis Tools
Section titled “Bundle Analysis Tools”Measuring bundle size accurately requires the right tools. HELiX uses a combination of build-time analysis and runtime monitoring.
Vite Build Analysis
Section titled “Vite Build Analysis”Vite provides detailed bundle analysis during production builds. The HELiX configuration outputs granular metrics for each component.
npm run buildOutput example:
dist/index.js 0.13 kB │ gzip: 0.10 kBdist/components/hx-button/index.js 3.47 kB │ gzip: 1.42 kBdist/components/hx-card/index.js 2.89 kB │ gzip: 1.18 kBdist/components/hx-text-input/index.js 4.21 kB │ gzip: 1.73 kBdist/shared/hx-form-Dy74Gm0T.js 1.56 kB │ gzip: 0.68 kBKey metrics to monitor:
- Unminified size: Shows raw code size before minification
- Minified size: Size after terser/esbuild minification
- Gzipped size: Compressed size (what’s actually transferred over HTTP)
- Brotli size: More aggressive compression (10-20% smaller than gzip)
Bundlephobia
Section titled “Bundlephobia”Bundlephobia analyzes npm packages and shows their impact on bundle size.
How to use:
- Publish a pre-release version of
@helixui/library - Enter package name at bundlephobia.com
- View per-component tree-shaking impact
Example analysis for hx-button:
Import: import { HelixButton } from '@helixui/library'Bundle size: 8.2 KB (minified)Gzipped: 3.1 KBTree-shakeable: ✅Dependencies: lit@3.3.2 (5.1 KB)rollup-plugin-visualizer
Section titled “rollup-plugin-visualizer”Visual bundle analysis shows which modules contribute to bundle size.
Installation:
npm install --save-dev rollup-plugin-visualizerVite integration:
import { visualizer } from 'rollup-plugin-visualizer';
export default defineConfig({ plugins: [ visualizer({ filename: 'dist/stats.html', gzipSize: true, brotliSize: true, }), ],});Output: Interactive HTML visualization showing module sizes, dependencies, and chunk relationships.
Chrome DevTools Coverage
Section titled “Chrome DevTools Coverage”Runtime coverage analysis identifies unused code in production.
Steps:
- Open Chrome DevTools → Coverage panel (Cmd+Shift+P → “Show Coverage”)
- Load your application
- Interact with components
- View unused bytes per file
Red flags:
-
50% unused code in a bundle suggests poor code splitting
- Entire components loaded but never rendered indicate missing lazy loading
Tree-Shaking Optimization
Section titled “Tree-Shaking Optimization”Tree-shaking eliminates dead code during the build process. For it to work effectively, both the library and consumers must follow strict conventions.
How Tree-Shaking Works
Section titled “How Tree-Shaking Works”Tree-shaking relies on static analysis of ES module imports/exports. Bundlers like Rollup, Webpack, and Vite analyze which exports are actually imported and remove everything else.
Example:
export function usedFunction() { /* ... */}export function unusedFunction() { /* ... */}
// consumer.tsimport { usedFunction } from './utils.js';
usedFunction(); // Only this code is included in bundle// unusedFunction is removed (tree-shaken)Requirements for Effective Tree-Shaking
Section titled “Requirements for Effective Tree-Shaking”1. ES Modules Only
Section titled “1. ES Modules Only”Tree-shaking only works with ES module syntax (import/export). CommonJS (require/module.exports) cannot be tree-shaken.
HELiX configuration:
{ "type": "module", "exports": { ".": { "types": "./src/index.ts", "import": "./src/index.ts" }, "./components/*": { "types": "./src/components/*/index.ts", "import": "./src/components/*/index.ts" } }}2. sideEffects: false
Section titled “2. sideEffects: false”The sideEffects field tells bundlers that modules are pure and safe to remove if unused.
{ "sideEffects": false}What this means: If a module is imported but none of its exports are used, the bundler can remove it entirely.
Exception: If a module has side effects (registers global event listeners, mutates globals, etc.), it should be listed:
{ "sideEffects": ["./src/polyfills.js"]}3. Per-Component Entry Points
Section titled “3. Per-Component Entry Points”HELiX exposes each component as a separate entry point to enable granular imports.
Vite configuration:
export default defineConfig({ build: { lib: { entry: { index: resolve(__dirname, 'src/index.ts'), 'components/hx-button/index': resolve(__dirname, 'src/components/hx-button/index.ts'), 'components/hx-card/index': resolve(__dirname, 'src/components/hx-card/index.ts'), // ... per-component entries }, formats: ['es'], }, },});Consumer usage:
// ✅ Tree-shakeable (only loads hx-button)import '@helixui/library/components/hx-button';
// ❌ NOT tree-shakeable (loads entire library)import '@helixui/library';4. No Barrel Exports
Section titled “4. No Barrel Exports”Barrel exports (re-exporting everything from index.ts) defeat tree-shaking.
Anti-pattern:
// ❌ src/index.ts (barrel export)export * from './components/hx-button/index.js';export * from './components/hx-card/index.js';export * from './components/hx-text-input/index.js';// ... (all components)
// Consumer can't tree-shake thisimport { HelixButton } from '@helixui/library';HELiX approach:
// ✅ src/components/hx-button/index.tsexport { HelixButton } from './hx-button.js';
// Consumer imports directlyimport { HelixButton } from '@helixui/library/components/hx-button';Verifying Tree-Shaking
Section titled “Verifying Tree-Shaking”Test:
- Create a minimal consumer app
- Import a single component
- Build for production
- Verify bundle only includes that component + Lit core
import '@helixui/library/components/hx-button';
document.body.innerHTML = '<hx-button>Test</hx-button>';Expected bundle size: ~8-10 KB (5 KB Lit + 3-5 KB hx-button)
If larger: Tree-shaking is broken. Common causes:
- Barrel exports in library
- Side effects in modules
- Non-ESM dependencies
Code Splitting Strategies
Section titled “Code Splitting Strategies”Code splitting divides your application into smaller chunks that can be loaded on demand. This reduces the initial bundle size and improves Time to Interactive.
Route-Based Code Splitting
Section titled “Route-Based Code Splitting”For applications with multiple pages, split code by route so users only load JavaScript for the current page.
React example:
import { lazy, Suspense } from 'react';
const Dashboard = lazy(() => import('./pages/Dashboard'));const Profile = lazy(() => import('./pages/Profile'));
function App() { return ( <Suspense fallback={<div>Loading...</div>}> <Route path="/dashboard" component={Dashboard} /> <Route path="/profile" component={Profile} /> </Suspense> );}Impact: If Dashboard uses hx-card and Profile uses hx-form, each route only loads the components it needs.
Component-Level Code Splitting
Section titled “Component-Level Code Splitting”For heavy components used in specific contexts, split them into separate chunks.
When to split:
- Component is >10 KB (minified)
- Component is only used in specific user flows
- Component has heavy dependencies (charts, editors, etc.)
Example (Vite):
// Dynamically import heavy component (the @customElement decorator registers it on import)async function loadDataTable() { await import('@helixui/library/components/hx-data-table');}
// Load only when neededif (userWantsDataTable) { await loadDataTable();}Vendor Code Splitting
Section titled “Vendor Code Splitting”Separate vendor code (Lit, dependencies) from application code so users can cache vendor bundles across page loads.
Vite configuration:
export default defineConfig({ build: { rollupOptions: { output: { manualChunks: { 'vendor-lit': ['lit', '@lit/reactive-element'], 'vendor-hx': ['@helixui/tokens'], }, }, }, },});Result:
vendor-lit.js(5 KB, rarely changes, cached long-term)vendor-wc.js(2 KB, changes with token updates)app.js(application code, changes frequently)
When NOT to Code Split
Section titled “When NOT to Code Split”Small components (<10 KB): Overhead of additional HTTP requests may outweigh benefits.
Critical path code: Components needed for first paint should be inlined, not split.
Shared dependencies: If multiple chunks import the same module, bundlers create shared chunks. Too many shared chunks increase HTTP request overhead.
Rule of thumb: Aim for 3-5 main chunks + vendor bundles. More than 10 chunks suggests over-splitting.
Lazy Loading Patterns
Section titled “Lazy Loading Patterns”Lazy loading defers component registration until it’s needed. This is especially effective for components used conditionally or below the fold.
Intersection Observer Lazy Loading
Section titled “Intersection Observer Lazy Loading”Load components when they enter the viewport.
// Register observer for lazy componentsconst lazyComponents = new Map([ ['hx-data-table', () => import('@helixui/library/components/hx-data-table')],]);
const observer = new IntersectionObserver((entries) => { entries.forEach(async (entry) => { if (entry.isIntersecting) { const tagName = entry.target.tagName.toLowerCase(); const loader = lazyComponents.get(tagName);
if (loader) { await loader(); observer.unobserve(entry.target); // Stop observing once loaded } } });});
// Observe all lazy component placeholdersdocument.querySelectorAll('hx-data-table:not(:defined)').forEach((el) => observer.observe(el));When to use:
- Components below the fold
- Components in accordions/tabs (not visible on load)
- Heavy components that aren’t critical
User Interaction Lazy Loading
Section titled “User Interaction Lazy Loading”Load components when the user interacts with a trigger.
// Load modal component on button clickdocument.getElementById('open-modal')?.addEventListener('click', async () => { await import('@helixui/library/components/hx-dialog');
const modal = document.createElement('hx-dialog'); modal.innerHTML = '<p>Modal content</p>'; document.body.appendChild(modal);});When to use:
- Modals, dialogs, popovers
- Admin-only components
- Components used in specific workflows
Prefetching and Preloading
Section titled “Prefetching and Preloading”Prefetch likely-needed components during idle time to avoid loading delay when user interacts.
// Prefetch modal component during browser idle timeif ('requestIdleCallback' in window) { requestIdleCallback(() => { const link = document.createElement('link'); link.rel = 'prefetch'; link.href = '/dist/components/hx-dialog/index.js'; document.head.appendChild(link); });}Strategies:
- Prefetch: Low-priority background download for likely-needed resources
- Preload: High-priority download for resources needed soon
- ModulePreload: Preload ES modules and their dependencies
<!-- Preload critical component --><link rel="modulepreload" href="/dist/components/hx-button/index.js" />
<!-- Prefetch likely-needed component --><link rel="prefetch" href="/dist/components/hx-dialog/index.js" />Minification
Section titled “Minification”Minification removes whitespace, comments, and shortens variable names to reduce code size. HELiX uses esbuild for fast, efficient minification.
Vite Minification Configuration
Section titled “Vite Minification Configuration”export default defineConfig({ build: { minify: 'esbuild', // Fast minifier (default in Vite) target: 'es2020', // Modern syntax = smaller output },});esbuild vs. terser:
- esbuild: 10-100x faster, good compression (~95% of terser)
- terser: Slower, slightly better compression (~5% smaller)
HELiX uses esbuild for development speed. For production releases, consider terser for maximum compression:
export default defineConfig({ build: { minify: 'terser', terserOptions: { compress: { drop_console: true, // Remove console.log drop_debugger: true, // Remove debugger statements pure_funcs: ['console.log'], // Remove specific functions }, mangle: { toplevel: true, // Mangle top-level names }, }, },});Minification Impact
Section titled “Minification Impact”Example: hx-button component
| Version | Size |
|---|---|
| Original source | 12.4 KB |
| Minified (esbuild) | 4.2 KB (66% reduction) |
| Minified + gzip | 1.8 KB (85% reduction) |
| Minified + brotli | 1.6 KB (87% reduction) |
What Gets Minified
Section titled “What Gets Minified”- Whitespace removal: Spaces, tabs, newlines
- Comment removal: All comments stripped
- Variable mangling: Long names shortened (
myVariableName→a) - Dead code elimination: Unreachable code removed
- Constant folding:
2 + 2→4 - Property mangling (advanced): Shorten object property names (risky)
Note: Property mangling is disabled by default because it can break code that relies on property name strings.
Compression
Section titled “Compression”Compression encodes assets in a more efficient format for network transfer. All modern browsers support gzip and Brotli compression.
Gzip Compression
Section titled “Gzip Compression”Gzip has been the standard HTTP compression algorithm for decades. It’s universally supported and provides good compression ratios.
Compression ratios (typical JavaScript):
- Small files (<10 KB): 50-60% size reduction
- Medium files (10-100 KB): 65-75% size reduction
- Large files (>100 KB): 70-80% size reduction
Server configuration (Express example):
import compression from 'compression';
app.use( compression({ level: 6, // Compression level (1-9, 6 is default) threshold: 1024, // Only compress files >1KB }),);Static pre-compression:
# Pre-compress files during buildgzip -k dist/**/*.jsNginx configuration:
gzip on;gzip_vary on;gzip_min_length 1024;gzip_types text/plain text/css application/json application/javascript;gzip_comp_level 6;Brotli Compression
Section titled “Brotli Compression”Brotli is a newer compression algorithm from Google that achieves 10-25% better compression than gzip for text-based assets.
Compression ratios (JavaScript):
- Small files: 55-65% reduction (5-10% better than gzip)
- Medium files: 70-80% reduction (10-15% better than gzip)
- Large files: 75-85% reduction (15-25% better than gzip)
Why Brotli is better:
- Larger dictionary window (reduces redundancy)
- Context-aware compression (better for HTML/CSS/JS)
- Multiple compression levels (1-11)
When to use Brotli:
- Static assets (pre-compress during build at level 11)
- Modern browsers (all browsers since 2017 support Brotli)
- CDN delivery (most CDNs support Brotli)
When to fallback to gzip:
- Dynamic content (Brotli level 11 is too slow for on-the-fly compression)
- Legacy browser support (use gzip as fallback)
Vite Brotli plugin:
npm install --save-dev vite-plugin-compressionimport viteCompression from 'vite-plugin-compression';
export default defineConfig({ plugins: [ // Brotli compression viteCompression({ algorithm: 'brotliCompress', ext: '.br', threshold: 1024, compressionOptions: { level: 11 }, }), // Gzip fallback viteCompression({ algorithm: 'gzip', ext: '.gz', threshold: 1024, }), ],});Output:
dist/components/hx-button/index.js (4.2 KB)dist/components/hx-button/index.js.gz (1.8 KB) - 57% smallerdist/components/hx-button/index.js.br (1.6 KB) - 62% smallerServer configuration (Nginx):
brotli on;brotli_types text/plain text/css application/json application/javascript;brotli_comp_level 6; # Dynamic contentbrotli_static on; # Serve pre-compressed .br filesCompression Best Practices
Section titled “Compression Best Practices”- Pre-compress static assets: Use highest compression (Brotli 11, gzip 9) during build
- Dynamic compression: Use moderate levels (Brotli 4-6, gzip 6) for on-the-fly compression
- Cache compressed responses: Set
Cache-ControlandETagheaders - Serve correct version: Serve
.brto modern browsers,.gzas fallback - Don’t compress small files: <1 KB files have more overhead than benefit
- Don’t double-compress: Don’t compress images, videos, or already-compressed formats
Monitoring and CI Enforcement
Section titled “Monitoring and CI Enforcement”Bundle size limits are only effective if they’re continuously monitored and enforced.
CI Bundle Size Checks
Section titled “CI Bundle Size Checks”HELiX uses GitHub Actions to enforce bundle size budgets on every PR.
Workflow configuration:
name: Bundle Size Check
on: [pull_request]
jobs: bundle-size: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: 22
- name: Install dependencies run: npm ci
- name: Build library run: npm run build --workspace=@helixui/library
- name: Analyze bundle size run: | # Check total bundle size TOTAL_SIZE=$(du -sb packages/hx-library/dist | awk '{print $1}') MAX_SIZE=$((50 * 1024)) # 50 KB
if [ "$TOTAL_SIZE" -gt "$MAX_SIZE" ]; then echo "❌ Bundle size exceeds limit: $TOTAL_SIZE bytes > $MAX_SIZE bytes" exit 1 fi
echo "✅ Bundle size OK: $TOTAL_SIZE bytes"Lighthouse CI
Section titled “Lighthouse CI”Monitor Core Web Vitals and performance scores on every deploy.
Configuration:
module.exports = { ci: { collect: { url: ['http://localhost:3150/'], numberOfRuns: 3, }, assert: { assertions: { 'categories:performance': ['error', { minScore: 0.9 }], 'largest-contentful-paint': ['error', { maxNumericValue: 2500 }], interactive: ['error', { maxNumericValue: 3000 }], 'total-blocking-time': ['error', { maxNumericValue: 300 }], }, }, upload: { target: 'temporary-public-storage', }, },};GitHub Action:
- name: Run Lighthouse CI run: | npm install -g @lhci/cli lhci autorun env: LHCI_GITHUB_APP_TOKEN: ${{ secrets.LHCI_GITHUB_APP_TOKEN }}Bundle Size Regression Detection
Section titled “Bundle Size Regression Detection”Automatically detect and report bundle size changes in PRs.
Using bundlesize:
npm install --save-dev bundlesize{ "bundlesize": [ { "path": "packages/hx-library/dist/components/hx-button/index.js", "maxSize": "5 KB", "compression": "gzip" }, { "path": "packages/hx-library/dist/index.js", "maxSize": "50 KB", "compression": "gzip" } ]}GitHub Action:
- name: Check bundle size run: npx bundlesize env: CI_REPO_OWNER: ${{ github.repository_owner }} CI_REPO_NAME: ${{ github.event.repository.name }} CI_COMMIT_SHA: ${{ github.sha }} CI_BRANCH: ${{ github.ref }}Performance Dashboard
Section titled “Performance Dashboard”Track bundle size trends over time using the Admin Dashboard.
Metrics tracked:
- Total bundle size (minified + gzipped)
- Per-component bundle size
- Shared chunk size
- Dependency bundle size (Lit, tokens)
- Historical trends (size changes over releases)
Location: http://localhost:3159/health → Performance section
HELiX Bundle Metrics
Section titled “HELiX Bundle Metrics”Current bundle size metrics for HELiX (as of latest build):
Full Library Bundle
Section titled “Full Library Bundle”| Format | Size |
|---|---|
| Unminified | 156 KB |
| Minified (esbuild) | 52 KB |
| Gzipped | 18 KB |
| Brotli | 16 KB |
Status: ✅ Under 50 KB budget (gzipped)
Individual Components
Section titled “Individual Components”| Component | Minified | Gzipped | Brotli |
|---|---|---|---|
| hx-button | 4.2 KB | 1.8 KB | 1.6 KB |
| hx-card | 3.8 KB | 1.6 KB | 1.4 KB |
| hx-text-input | 5.1 KB | 2.1 KB | 1.9 KB |
| hx-checkbox | 3.9 KB | 1.7 KB | 1.5 KB |
| hx-select | 4.8 KB | 2.0 KB | 1.8 KB |
| hx-alert | 3.2 KB | 1.4 KB | 1.2 KB |
| hx-badge | 2.8 KB | 1.2 KB | 1.1 KB |
Status: ✅ All components under 5 KB budget (minified)
Dependency Breakdown
Section titled “Dependency Breakdown”| Dependency | Size (min+gz) | Percentage |
|---|---|---|
| Lit core | 5.1 KB | 28% |
| @helixui/tokens | 1.8 KB | 10% |
| Component code | 11.1 KB | 62% |
Note: Lit is externalized and only loaded once, even when using multiple components.
Tree-Shaking Effectiveness
Section titled “Tree-Shaking Effectiveness”| Import Style | Bundle Size (min+gz) |
|---|---|
| Single component | ~8 KB (Lit + component) |
| Three components | ~12 KB (Lit + components) |
| Full library | ~18 KB (Lit + all components) |
Tree-shaking savings: Importing only what you need saves ~55% vs. full bundle.
Best Practices Checklist
Section titled “Best Practices Checklist”When building components for HELiX, follow these bundle size optimization practices:
Library Configuration
Section titled “Library Configuration”- ✅ Set
sideEffects: falsein package.json - ✅ Use ES module syntax exclusively (
import/export) - ✅ Provide per-component entry points
- ✅ Externalize Lit and workspace dependencies
- ✅ Enable source maps for debugging (doesn’t affect bundle size)
- ✅ Use esbuild minification for development, terser for production
Component Implementation
Section titled “Component Implementation”- ✅ Import only what you use (avoid
import *) - ✅ Use Lit’s built-in directives (already tree-shakeable)
- ✅ Avoid heavy dependencies (chart libraries, date pickers, etc.)
- ✅ Lazy-load large assets (images, fonts) via CSS
- ✅ Use CSS custom properties instead of inline styles
- ✅ Keep component logic focused (single responsibility)
Build Configuration
Section titled “Build Configuration”- ✅ Enable code splitting for large applications
- ✅ Pre-compress static assets (gzip + Brotli)
- ✅ Set appropriate cache headers
- ✅ Use CDN for static assets
- ✅ Monitor bundle size in CI
- ✅ Fail builds that exceed budgets
Runtime Loading
Section titled “Runtime Loading”- ✅ Lazy-load components below the fold
- ✅ Use Intersection Observer for viewport-based loading
- ✅ Prefetch likely-needed components during idle time
- ✅ Defer non-critical components until after first paint
- ✅ Measure real-world performance with RUM
Debugging Bundle Size Issues
Section titled “Debugging Bundle Size Issues”If bundle size exceeds budgets, use this debugging workflow:
Step 1: Identify the Culprit
Section titled “Step 1: Identify the Culprit”# Build with analysisnpm run build --workspace=@helixui/library
# Check component sizesls -lh packages/hx-library/dist/components/**/index.jsStep 2: Analyze Dependencies
Section titled “Step 2: Analyze Dependencies”# Install bundle analyzernpm install --save-dev rollup-plugin-visualizer
# Generate visual reportnpm run buildopen packages/hx-library/dist/stats.htmlLook for:
- Large dependencies (>10 KB)
- Duplicate code across chunks
- Non-tree-shakeable imports
Step 3: Check Tree-Shaking
Section titled “Step 3: Check Tree-Shaking”// Create minimal test appimport '@helixui/library/components/hx-button';
// Build and measurenpm run build
// Expected: ~8 KB (Lit + hx-button)// Actual: ??? KBIf actual > expected:
- Check for barrel exports
- Verify
sideEffects: false - Ensure ES module syntax
Step 4: Profile Runtime Loading
Section titled “Step 4: Profile Runtime Loading”// Measure component load timeconst start = performance.now();await import('@helixui/library/components/hx-button');const end = performance.now();console.log(`Load time: ${end - start}ms`);Target: <50ms on desktop, <100ms on mobile
Step 5: Compare Against Baseline
Section titled “Step 5: Compare Against Baseline”# Checkout main branchgit checkout mainnpm run build
# Measure baseline sizeBASELINE=$(du -sb packages/hx-library/dist | awk '{print $1}')
# Checkout PR branchgit checkout feature-branchnpm run build
# Measure PR sizePR_SIZE=$(du -sb packages/hx-library/dist | awk '{print $1}')
# Calculate deltaecho "Size change: $((PR_SIZE - BASELINE)) bytes"Resources
Section titled “Resources”Web Performance
Section titled “Web Performance”- Reduce JavaScript payloads with tree shaking | web.dev
- Reduce JavaScript payloads with code splitting | web.dev
- Code splitting with React.lazy and Suspense | web.dev
- Minify and compress network payloads with brotli | web.dev
- Minify and compress network payloads with gzip | web.dev
- Optimize the encoding and transfer size of text-based assets | web.dev
Tools and Analysis
Section titled “Tools and Analysis”- Bundlephobia - Analyze npm package bundle sizes
- Bundle Size Comparison: Brotli vs Gzip
- Webpack Tree Shaking Guide
- 8 Ways to Optimize Your JavaScript Bundle Size | Codecov
Best Practices
Section titled “Best Practices”- Cut Initial Load Time by 40%: Lazy Loading vs Code Splitting
- Lazy Loading vs Code Splitting: Key Differences
- Brotli Compression: A Fast Alternative to GZIP
Next Steps
Section titled “Next Steps”- Render Performance - Optimize component rendering and re-rendering
- Network Performance - Minimize HTTP requests and optimize asset delivery
- Memory Management - Prevent memory leaks and optimize runtime memory usage
- Performance Testing - Automated performance regression testing
Remember: Bundle size optimization is not a one-time task. Monitor metrics continuously, enforce budgets in CI, and treat regressions as bugs. Every kilobyte counts.