DEV Community

Phumbie
Phumbie

Posted on

Optimizing Vue.js Component Library Bundle Size: A Real-World Case Study

How I reduced my Vue component library from 388KB to 124KB (68% reduction) and solved dependency resolution issues

The Problem: Massive Bundle Size and Dependency Hell

Initial Purpose: Consistency Across Multiple Frontend Products

When I first built the company's internal component library @zilla-tech/ace-ui, the primary goal was consistency. We had numerous frontend products across the organization, and we needed a way to use our components consistently across all projects. The library was designed to provide a unified design system and component API that all the team could rely on.

Initial Focus: Functionality, consistency, and developer experience
Bundle Size: Not a primary concern at the time

The Optimization Journey Begins

Several months after the initial development, as our library grew and more projects adopted it, bundle size became a critical concern. What started as a 388KB bundle was now causing performance issues and deployment headaches across our development team.

Initial Symptoms

  • Bundle size: 388KB (113.9KB gzipped) - way too large for a component library
  • Dependency resolution errors: Our projects couldn't resolve imports like libphonenumber-js, vue-tel-input, v-calendar
  • Vue mixin errors: Cannot read properties of undefined (reading 'mixin') in consuming projects
  • Complex installation: We had to manually install 7+ peer dependencies

The Root Cause

I had bundled all dependencies directly into the library, including heavy packages like:

  • v-calendar (~260KB)
  • vue-tel-input (~175KB)
  • @popperjs/core (~25KB)
  • dayjs (~12KB)
  • libphonenumber-js (~15KB)

This approach seemed logical at first - "just bundle everything so users don't have to install dependencies." But it created more problems than it solved.

The Journey: Three Optimization Approaches

Approach 1: Initial Peer Dependencies (v1.0.42)

My first attempt was to externalize all dependencies using peerDependencies:

{
  "peerDependencies": {
    "@popperjs/core": "^2.10.1",
    "dayjs": "^1.11.13",
    "libphonenumber-js": "^1.10.0",
    "v-calendar": "^3.1.2",
    "vue-router": "^4.0.0",
    "vue-select": "^4.0.0-beta.1",
    "vue-tel-input": "^9.3.0",
    "floating-vue": "^5.2.2",
    "remixicon": "^4.6.0"
  }
}
Enter fullscreen mode Exit fullscreen mode

Vite Configuration:

// vite.config.ts
rollupOptions: {
  external: [
    "vue", "v-calendar", "dayjs", "vue-select", 
    "vue-tel-input", "floating-vue", "@popperjs/core",
    "vue-router", "libphonenumber-js"
  ]
}
Enter fullscreen mode Exit fullscreen mode

Results:

  • ✅ Bundle size: ~30KB (87% reduction!)
  • ✅ Tree-shaking enabled
  • ❌ Complex installation process
  • ❌ Dependency resolution errors in host projects

The Hidden Problem: Nested Dependencies

After the initial optimization, I thought I was done. But then the team started encountering errors like:

✘ [ERROR] Could not resolve "libphonenumber-js"
✘ [ERROR] Could not resolve "vue-tel-input"
✘ [ERROR] Could not resolve "v-calendar"
✘ [ERROR] Could not resolve "vuex"
Enter fullscreen mode Exit fullscreen mode

The Root Cause: I didn't understand that some packages have their own internal dependencies that also need to be externalized. For example:

  • vue-tel-input internally depends on libphonenumber-js
  • v-calendar internally depends on @popperjs/core
  • Some components were using vuex for state management

When I externalized vue-tel-input, I forgot that it would try to import libphonenumber-js internally, causing resolution errors.

Approach 2: Bundled Dependencies (v1.0.43 - First Attempt)

When our team complained about installation complexity and dependency resolution errors, I tried bundling dependencies:

{
  "dependencies": {
    "@popperjs/core": "^2.10.1",
    "dayjs": "^1.11.13",
    "libphonenumber-js": "^1.10.0",
    "v-calendar": "^3.1.2",
    "vue-router": "^4.0.0",
    "vue-select": "^4.0.0-beta.1",
    "vue-tel-input": "^9.3.0"
  }
}
Enter fullscreen mode Exit fullscreen mode

Results:

  • ✅ Simple installation: npm install @zilla-tech/ace-ui
  • ✅ No dependency resolution errors
  • ❌ Bundle size: ~400KB (even larger!)
  • ❌ Version conflicts
  • ❌ Duplicate dependencies

The Vue Mixin Error Mystery

Even with bundled dependencies, our team was still getting this cryptic error:

Uncaught TypeError: Cannot read properties of undefined (reading 'mixin')
at screens.js:53:5
Enter fullscreen mode Exit fullscreen mode

This was caused by:

  • Multiple Vue instances in the host project
  • Missing Vue deduplication in Vite config
  • Incorrect Vue alias configuration

Approach 3: Optimized Peer Dependencies (v1.0.43 - Final)

I realized the issue wasn't the peer dependencies approach itself, but my incomplete understanding of nested dependencies and proper configuration. I went back to peer dependencies but with:

  1. Complete dependency mapping - Including all nested dependencies
  2. Better documentation - Clear installation instructions
  3. Host project guidance - Proper Vite configuration examples
  4. Vuex refactoring - Removed Vuex dependencies from components

The Vuex Refactoring Challenge

One of the biggest issues was that several components were using Vuex for state management:

// Before: Components using Vuex
import { mapActions } from 'vuex';

export default {
  methods: {
    ...mapActions('notification', ['showToast']),
    handleError() {
      this.showToast({ message: 'Error occurred' });
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

This created a hard dependency on Vuex, which users might not have installed. I refactored these components to use internal services instead:

// After: Using internal services
import { showToast } from '../../Notification/Toast/toastService';

export default {
  methods: {
    handleError() {
      showToast({ message: 'Error occurred' });
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Components Refactored:

  • Select component - Toast notifications
  • Upload component - Toast notifications
  • GoogleAutoComplete component - Alert notifications
  • CardSelect component - Data fetching (converted to props/events)

Complete Dependency Mapping

The key insight was understanding the complete dependency tree:

ace-ui components
├── vue-tel-input
│   └── libphonenumber-js (nested dependency!)
├── v-calendar  
│   └── @popperjs/core (nested dependency!)
├── vue-select
├── dayjs
├── vue-router
└── vuex (removed through refactoring)
Enter fullscreen mode Exit fullscreen mode

I had to externalize ALL dependencies, including the nested ones that packages import internally.

The Solution: Optimized Peer Dependencies

1. Proper Vite Configuration

// vite.config.ts
export default defineConfig({
  build: {
    lib: {
      entry: resolve(__dirname, 'src/components/index.js'),
      name: 'AceUI',
      fileName: 'ace-ui',
      formats: ["es", "cjs"],
    },
    rollupOptions: {
      // Externalize all peer dependencies
      external: [
        "vue", "v-calendar", "dayjs", "vue-select",
        "vue-tel-input", "floating-vue", "@popperjs/core",
        "vue-router", "libphonenumber-js", "vuex"
      ],
      output: [
        {
          format: "es",
          entryFileNames: "[name].mjs",
          preserveModules: true,
          preserveModulesRoot: "src",
          exports: "named",
          dir: "../ace-ui/es",
        },
        {
          format: "cjs", 
          entryFileNames: "[name].js",
          preserveModules: true,
          preserveModulesRoot: "src",
          exports: "named",
          dir: "../ace-ui/lib",
        }
      ]
    },
    minify: 'terser',
    terserOptions: {
      compress: {
        drop_console: true,
        drop_debugger: true,
      }
    }
  }
});
Enter fullscreen mode Exit fullscreen mode

2. Clear Installation Instructions

Instead of leaving our team to figure out dependencies, I provided comprehensive installation instructions:

# Install ace-ui
npm install @zilla-tech/ace-ui

# Install required peer dependencies
npm install vue@^3.0.0 @popperjs/core@^2.10.1 dayjs@^1.11.13 libphonenumber-js@^1.10.0 v-calendar@^3.1.2 vue-router@^4.0.0 vue-select@^4.0.0-beta.1 vue-tel-input@^9.3.0

# Install optional dependencies (only if using those components)
npm install floating-vue@^5.2.2 remixicon@^4.6.0
Enter fullscreen mode Exit fullscreen mode

3. Host Project Vite Configuration

I also provided guidance for our projects to properly configure Vite:

// Host project vite.config.js
export default defineConfig({
  optimizeDeps: {
    include: [
      '@zilla-tech/ace-ui',
      'vue-tel-input',
      'libphonenumber-js', 
      'v-calendar',
      'dayjs',
      'vue-select',
      'floating-vue',
      '@popperjs/core'
    ],
  },
  resolve: {
    alias: {
      'vue': fileURLToPath(new URL('./node_modules/vue/dist/vue.esm-bundler.js', import.meta.url))
    },
    dedupe: ['vue'], // Prevent multiple Vue instances
  },
})
Enter fullscreen mode Exit fullscreen mode

4. Troubleshooting Process

When our team reported errors, I had to debug systematically:

Step 1: Identify Missing Dependencies

# Check what's actually being imported
grep -r "import.*from" src/components/ | grep -E "(vue-tel-input|libphonenumber|v-calendar)"
Enter fullscreen mode Exit fullscreen mode

Step 2: Check Nested Dependencies

# Look at package.json of dependencies
npm info vue-tel-input dependencies
npm info v-calendar dependencies
Enter fullscreen mode Exit fullscreen mode

Step 3: Test Host Project Configuration

// Add debugging to vite.config.js
console.log('External dependencies:', rollupOptions.external);
console.log('Optimized deps:', optimizeDeps.include);
Enter fullscreen mode Exit fullscreen mode

Step 4: Clear Caches

# Clear all caches when debugging
rm -rf node_modules/.vite
rm -rf node_modules/@zilla-tech
npm install
Enter fullscreen mode Exit fullscreen mode

5. Common Project Issues and Solutions

Issue 1: Missing Peer Dependencies

# Error: Could not resolve "libphonenumber-js"
# Solution: Install the missing dependency
npm install libphonenumber-js
Enter fullscreen mode Exit fullscreen mode

Issue 2: Vue Instance Conflicts

// Error: Cannot read properties of undefined (reading 'mixin')
// Solution: Add Vue deduplication
resolve: {
  dedupe: ['vue'],
  alias: {
    'vue': path.resolve('./node_modules/vue/dist/vue.esm-bundler.js')
  }
}
Enter fullscreen mode Exit fullscreen mode

Issue 3: Incorrect Externalization

// Error: Dependencies bundled when they should be external
// Solution: Check Vite external array includes all peer dependencies
external: [
  "vue", "v-calendar", "dayjs", "vue-select",
  "vue-tel-input", "floating-vue", "@popperjs/core",
  "vue-router", "libphonenumber-js" // Don't forget nested deps!
]
Enter fullscreen mode Exit fullscreen mode

The Results: Massive Success

Bundle Size Reduction

  • Before: 388KB (113.9KB gzipped)
  • After: 124KB (35KB gzipped)
  • Reduction: 68% smaller!

Developer Experience Improvements

  • ✅ Clear installation instructions
  • ✅ No more dependency resolution errors
  • ✅ Proper Vue instance management
  • ✅ Tree-shaking enabled
  • ✅ Version control for dependencies

Performance Benefits

  • ✅ Faster initial load times
  • ✅ Better caching (dependencies cached separately)
  • ✅ Reduced memory usage
  • ✅ Smaller JavaScript bundles

Key Learnings

1. Nested Dependencies Are Critical

The biggest lesson: When externalizing dependencies, you must account for ALL nested dependencies. A package like vue-tel-input might internally import libphonenumber-js, which also needs to be externalized.

How to identify nested dependencies:

# Check what dependencies a package actually uses
npm info vue-tel-input dependencies
npm info v-calendar dependencies

# Look at the actual imports in node_modules
grep -r "import.*from" node_modules/vue-tel-input/
Enter fullscreen mode Exit fullscreen mode

2. Peer Dependencies vs Dependencies

Peer Dependencies are perfect for libraries when:

  • Bundle size matters
  • You want version control
  • Dependencies are commonly used across projects
  • Tree-shaking is important

Dependencies are better when:

  • Installation simplicity is critical
  • Dependencies are rarely used elsewhere
  • Bundle size is less important

3. Component Architecture Matters

Avoid hard dependencies in components:

  • Don't use vuex directly in components
  • Use internal services instead
  • Convert data fetching to props/events
  • Make components truly reusable

4. Documentation is Everything

The peer dependencies approach only works with excellent documentation. Your team needs:

  • Clear installation instructions
  • Explanation of why peer dependencies are used
  • Project configuration guidance
  • Troubleshooting tips
  • Complete dependency list

5. Vite Configuration Matters

Proper Vite configuration is crucial:

  • Externalize peer dependencies correctly (including nested ones!)
  • Use preserveModules for tree-shaking
  • Configure proper output formats
  • Set up minification properly
  • Include ALL dependencies in external array

6. Project Setup

Your projects need proper configuration:

  • Include dependencies in optimizeDeps.include
  • Use Vue deduplication (dedupe: ['vue'])
  • Configure proper aliases
  • Clear Vite cache when needed
  • Install ALL peer dependencies

7. Debugging Strategy

When things go wrong:

  1. Check the complete dependency tree
  2. Verify all nested dependencies are externalized
  3. Test host project configuration
  4. Clear all caches
  5. Check for Vue instance conflicts

8. The Trade-off is Worth It

While peer dependencies require more setup, the benefits outweigh the costs:

  • Massive bundle size reduction (68% in our case)
  • Better performance
  • Version control
  • Tree-shaking
  • No duplicate dependencies

Best Practices for Vue Component Libraries

1. Use Peer Dependencies Strategically

{
  "peerDependencies": {
    "vue": "^3.0.0",           // Always peer - version control critical
    "heavy-library": "^1.0.0", // Peer - large, commonly used
    "small-utility": "^2.0.0"  // Could be bundled - small, rarely used
  }
}
Enter fullscreen mode Exit fullscreen mode

2. Provide Clear Installation Instructions

# One-liner for all dependencies
npm install @your-library vue@^3.0.0 dependency1 dependency2 dependency3
Enter fullscreen mode Exit fullscreen mode

3. Configure Vite Properly

// Externalize peer dependencies
external: ["vue", "heavy-dependency"],
// Use preserveModules for tree-shaking
preserveModules: true,
// Minify aggressively
minify: 'terser'
Enter fullscreen mode Exit fullscreen mode

4. Document Host Project Setup

Provide clear guidance for consuming projects:

  • Vite configuration examples
  • Troubleshooting common issues
  • Performance optimization tips

5. Test Both Approaches

Before deciding on dependencies vs peerDependencies:

  • Measure bundle sizes
  • Test installation complexity
  • Check for version conflicts
  • Validate tree-shaking

Conclusion

Optimizing a Vue component library's bundle size requires careful consideration of dependencies, proper Vite configuration, and excellent documentation. While peer dependencies add installation complexity, they provide massive benefits in bundle size, performance, and flexibility.

The key is finding the right balance between developer experience and performance. In my case, the 68% bundle size reduction and improved performance made the additional installation steps worthwhile.

Final Stats:

  • Bundle size: 388KB → 124KB (68% reduction)
  • Gzipped size: 113.9KB → 35KB (69% reduction)
  • Installation: Complex but well-documented
  • Performance: Significantly improved
  • Developer experience: Much better with proper setup

The journey taught me that optimization isn't just about code - it's about the entire developer experience, from installation to performance to documentation.


Have you faced similar challenges with Vue component libraries? What strategies worked for you? Let me know in the comments!

Top comments (0)