Optimizing bundle size for frontend libraries requires a systematic approach to module resolution, static analysis, and automated guardrails. As a core component of the broader Tree-Shaking & Bundle Optimization strategy, library authors must architect exports that preserve consumer build tool capabilities while eliminating dead code at the source. This guide provides production-ready configurations, CI/CD integration patterns, and runtime profiling techniques to enforce strict size budgets across iterative releases.

Dual Module Resolution & Export Topology

Modern bundlers (Webpack 5+, Vite, Rollup, esbuild) rely on conditional exports to determine whether to consume ESM or CJS artifacts. Misconfigured resolution forces bundlers to fall back to CommonJS wrappers, which breaks static dependency graph traversal and disables tree-shaking entirely.

Conditional Exports & Static Path Mapping

Define explicit entry points in package.json using the exports field. This prevents dynamic require() traps and ensures consumers resolve the correct module format.

{
  "name": "@org/ui-core",
  "type": "module",
  "exports": {
    ".": {
      "import": "./dist/esm/index.js",
      "require": "./dist/cjs/index.js",
      "types": "./dist/types/index.d.ts"
    },
    "./utils/*": {
      "import": "./dist/esm/utils/*.js",
      "require": "./dist/cjs/utils/*.js",
      "types": "./dist/types/utils/*.d.ts"
    }
  }
}

️ Hazard Prevention:** Never rely on main or module fallbacks alone. Bundlers prioritizing main will load CJS, wrapping ESM in __esModule shims that prevent dead code elimination. Always pair exports with TypeScript path mapping alignment (paths in tsconfig.json) to guarantee IDE resolution matches runtime behavior.

Aggregated index.ts files that re-export dozens of modules force bundlers to parse the entire dependency graph, regardless of actual consumer usage. To preserve tree-shakeability across consumer build tools, refactor aggregated exports into direct module references. See Eliminating Barrel File Anti-Patterns for architectural patterns that enforce direct path imports (e.g., import { formatDate } from '@org/utils/date').

Tree-Shaking Configuration & Side-Effect Management

Bundlers assume modules may mutate global state unless explicitly told otherwise. Signaling pure code enables aggressive dead code elimination (DCE) during static dependency graph traversal.

Pure Annotations & Side-Effect Declarations

Apply /*#__PURE__*/ annotations to factory functions, class constructors, and static initializers. This instructs Terser, SWC, and esbuild to safely drop unused invocations.

// ✅ Safe for DCE
export const createLogger = /*#__PURE__*/ (() => {
 return (level: 'info' | 'warn') => console[level];
})();

// ❌ Retained by default (assumed side-effect)
export const legacyConfig = loadLegacyConfig();

️ Hazard Prevention:** Omitting /*#__PURE__*/ on static class properties or factory functions causes dead code elimination to retain unused initialization logic. Systematically annotate all non-side-effecting initializers and leverage TypeScript compiler plugins (e.g., ts-pure) to auto-inject pure markers during the build step.

Explicitly declare which files contain unavoidable side effects using the sideEffects field. This isolates CSS, polyfills, and global state mutations from the main logic graph.

{
  "sideEffects": [
    "*.css",
    "src/polyfills/*.ts",
    "src/setup/globals.ts"
  ]
}

️ Hazard Prevention:** Setting "sideEffects": true explicitly disables tree-shaking for the entire package. Use an explicit array of file paths, or set to false only when the package is strictly pure and stateless. For precise configuration strategies, refer to Implementing the sideEffects Flag Correctly.

CI/CD Integration & Automated Size Budgets

Manual size tracking fails under iterative development pressure. Establish automated guardrails in continuous integration pipelines to prevent bundle bloat across releases and platform deployments.

PR-Level Size Regression Checks

Use size-limit combined with GitHub Actions to enforce thresholds on every pull request. Track both raw parse size and compressed transfer size to model accurate network costs.

// .size-limit.json
[
  {
    "path": "dist/esm/index.js",
    "limit": "12 kB",
    "gzip": true,
    "brotli": true,
    "running": false
  }
]
# .github/workflows/bundle-budget.yml
name: Bundle Size Guardrails
on: [pull_request]
jobs:
  size-check:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
      - run: npm ci
      - run: npm run build
      - name: Enforce Size Budget
        run: npx size-limit --json > size-report.json
      - name: Fail on Threshold Violation
        run: |
          if [ "$(jq '.size > .limit' size-report.json)" = "true" ]; then
            echo "::error::Bundle exceeds size budget. Run 'size-limit' locally to investigate."
            exit 1
          fi

️ Hazard Prevention:** Tracking only uncompressed raw bytes leads to misleading delivery metrics and inaccurate network cost modeling. Always configure size-limit with --gzip and --brotli flags to measure actual network transfer costs and browser parse overhead. Integrate webpack-bundle-analyzer --mode static in staging pipelines to visualize module overlap before merging.

Consumer Impact & Runtime Profiling

Static bundle metrics do not guarantee optimal runtime performance. Validate library optimizations against real-world consumer environments and measure actual execution overhead beyond static bundle metrics.

Correlating Size with Core Web Vitals

Bundle reductions directly impact Time to Interactive (TTI) and First Contentful Paint (FCP) by reducing main-thread parse and compile time. Use Lighthouse CI to enforce performance thresholds alongside size budgets.

# lighthouserc.yml
ci:
  collect:
    numberOfRuns: 3
    settings:
      preset: desktop
  assert:
    assertions:
      'interactive': ['error', { maxNumericValue: 3500 }]
      'first-contentful-paint': ['error', { maxNumericValue: 1800 }]

In monorepo and micro-frontend architectures, analyze dependency deduplication and module graph overlap. Shared libraries must be externalized or hoisted to prevent duplicate runtime instances. Benchmark parse versus execution time using Chrome DevTools Performance panel or node --prof to identify hidden runtime costs from heavy initialization logic.

️ Hazard Prevention:** Static size != runtime cost. A 50kB library with synchronous DOM manipulation or heavy regex compilation on import will degrade TTI more than a 150kB lazy-loaded component. Defer initialization until first usage and validate consumer-side footprint using external tooling like Measuring Bundle Impact with Bundlephobia and Webpack Analyzer.

By enforcing conditional exports, precise side-effect declarations, automated CI budgets, and runtime-aware profiling, library maintainers can guarantee minimal consumer overhead while maintaining strict engineering standards across distributed frontend ecosystems.