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"
}
}
Vite Configuration:
// vite.config.ts
rollupOptions: {
external: [
"vue", "v-calendar", "dayjs", "vue-select",
"vue-tel-input", "floating-vue", "@popperjs/core",
"vue-router", "libphonenumber-js"
]
}
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"
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-inputinternally depends onlibphonenumber-js -
v-calendarinternally depends on@popperjs/core - Some components were using
vuexfor 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"
}
}
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
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:
- Complete dependency mapping - Including all nested dependencies
- Better documentation - Clear installation instructions
- Host project guidance - Proper Vite configuration examples
- 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' });
}
}
}
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' });
}
}
}
Components Refactored:
-
Selectcomponent - Toast notifications -
Uploadcomponent - Toast notifications -
GoogleAutoCompletecomponent - Alert notifications -
CardSelectcomponent - 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)
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,
}
}
}
});
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
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
},
})
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)"
Step 2: Check Nested Dependencies
# Look at package.json of dependencies
npm info vue-tel-input dependencies
npm info v-calendar dependencies
Step 3: Test Host Project Configuration
// Add debugging to vite.config.js
console.log('External dependencies:', rollupOptions.external);
console.log('Optimized deps:', optimizeDeps.include);
Step 4: Clear Caches
# Clear all caches when debugging
rm -rf node_modules/.vite
rm -rf node_modules/@zilla-tech
npm install
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
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')
}
}
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!
]
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/
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
vuexdirectly 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
preserveModulesfor 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:
- Check the complete dependency tree
- Verify all nested dependencies are externalized
- Test host project configuration
- Clear all caches
- 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
}
}
2. Provide Clear Installation Instructions
# One-liner for all dependencies
npm install @your-library vue@^3.0.0 dependency1 dependency2 dependency3
3. Configure Vite Properly
// Externalize peer dependencies
external: ["vue", "heavy-dependency"],
// Use preserveModules for tree-shaking
preserveModules: true,
// Minify aggressively
minify: 'terser'
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)