Vue.js Build Optimization: From 5MB to 200KB Bundle Size
Our Vue app bundle was 5MB. Initial load took 15 seconds on 3G. Users complained about slow loading. We were losing customers.
I spent two days optimizing the build. Bundle size dropped to 200KB. Load time: 2 seconds. Conversion rate increased by 23%.
Table of Contents
The Problem
Production build analysis:
npm run build -- --report
Results:
- Total bundle: 5.2MB
- vendor.js: 3.8MB (moment.js, lodash, etc.)
- app.js: 1.4MB
- Load time on 3G: 15 seconds
Unacceptable.
Bundle Analysis
Install analyzer:
npm install --save-dev webpack-bundle-analyzer
vue.config.js:
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin
module.exports = {
configureWebpack: {
plugins: [
new BundleAnalyzerPlugin()
]
}
}
Build and analyze:
npm run build
Opens visualization showing what’s in your bundle!
Code Splitting
Split by route:
Before:
import UserList from './components/UserList.vue'
import UserDetail from './components/UserDetail.vue'
const routes = [
{ path: '/users', component: UserList },
{ path: '/users/:id', component: UserDetail }
]
After:
const routes = [
{
path: '/users',
component: () => import(/* webpackChunkName: "users" */ './components/UserList.vue')
},
{
path: '/users/:id',
component: () => import(/* webpackChunkName: "user-detail" */ './components/UserDetail.vue')
}
]
Result: Multiple small chunks instead of one big bundle!
Tree Shaking
Import only what you need:
Bad:
import _ from 'lodash'
_.debounce(fn, 300)
Imports entire lodash (70KB)!
Good:
import debounce from 'lodash/debounce'
debounce(fn, 300)
Only imports debounce (5KB).
Moment.js Replacement
Moment.js is huge (288KB). Replace with day.js:
npm uninstall moment
npm install dayjs
Before:
import moment from 'moment'
moment().format('YYYY-MM-DD')
After:
import dayjs from 'dayjs'
dayjs().format('YYYY-MM-DD')
Savings: 280KB!
Remove Unused Dependencies
Audit dependencies:
npm install -g depcheck
depcheck
Remove unused:
npm uninstall unused-package-1 unused-package-2
Production Mode
Ensure production build:
NODE_ENV=production npm run build
Or in vue.config.js:
module.exports = {
productionSourceMap: false, // Disable source maps
configureWebpack: {
optimization: {
minimize: true
}
}
}
Compression
Enable gzip:
npm install --save-dev compression-webpack-plugin
vue.config.js:
const CompressionPlugin = require('compression-webpack-plugin')
module.exports = {
configureWebpack: {
plugins: [
new CompressionPlugin({
algorithm: 'gzip',
test: /\.(js|css|html|svg)$/,
threshold: 10240,
minRatio: 0.8
})
]
}
}
Serve gzipped files:
# nginx.conf
gzip on;
gzip_types text/plain text/css application/json application/javascript;
gzip_min_length 1000;
CDN for Dependencies
Use CDN for large libraries:
public/index.html:
<script src="https://cdn.jsdelivr.net/npm/vue@2.6.10/dist/vue.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/vue-router@3.1.3/dist/vue-router.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/vuex@3.1.2/dist/vuex.min.js"></script>
vue.config.js:
module.exports = {
configureWebpack: {
externals: {
vue: 'Vue',
'vue-router': 'VueRouter',
vuex: 'Vuex'
}
}
}
Image Optimization
Compress images:
npm install --save-dev image-webpack-loader
vue.config.js:
module.exports = {
chainWebpack: config => {
config.module
.rule('images')
.use('image-webpack-loader')
.loader('image-webpack-loader')
.options({
mozjpeg: { progressive: true, quality: 65 },
optipng: { enabled: false },
pngquant: { quality: [0.65, 0.90], speed: 4 },
gifsicle: { interlaced: false }
})
}
}
Use WebP:
<template>
<picture>
<source srcset="image.webp" type="image/webp">
<img src="image.jpg" alt="Fallback">
</picture>
</template>
Lazy Load Images
npm install vue-lazyload
import VueLazyload from 'vue-lazyload'
Vue.use(VueLazyload, {
preLoad: 1.3,
loading: '/loading.gif',
attempt: 1
})
<template>
<img v-lazy="imageUrl" alt="Product">
</template>
Prefetch and Preload
Control resource hints:
vue.config.js:
module.exports = {
chainWebpack: config => {
// Disable prefetch
config.plugins.delete('prefetch')
// Disable preload
config.plugins.delete('preload')
}
}
Manual prefetch:
const routes = [
{
path: '/users',
component: () => import(/* webpackPrefetch: true */ './UserList.vue')
}
]
CSS Optimization
Extract CSS:
module.exports = {
css: {
extract: true
}
}
Purge unused CSS:
npm install --save-dev @fullhuman/postcss-purgecss
postcss.config.js:
module.exports = {
plugins: [
require('@fullhuman/postcss-purgecss')({
content: ['./src/**/*.vue', './public/index.html'],
defaultExtractor: content => content.match(/[\w-/:]+(?<!:)/g) || []
})
]
}
Runtime Optimization
Use production build of Vue:
// webpack.config.js
module.exports = {
resolve: {
alias: {
'vue$': 'vue/dist/vue.runtime.esm.js'
}
}
}
Caching Strategy
Long-term caching:
vue.config.js:
module.exports = {
configureWebpack: {
output: {
filename: '[name].[contenthash:8].js',
chunkFilename: '[name].[contenthash:8].js'
}
}
}
Service Worker
Cache assets:
npm install --save-dev @vue/cli-plugin-pwa
vue add pwa
vue.config.js:
module.exports = {
pwa: {
workboxOptions: {
skipWaiting: true,
clientsClaim: true,
runtimeCaching: [
{
urlPattern: /^https:\/\/api\.example\.com/,
handler: 'NetworkFirst',
options: {
cacheName: 'api-cache',
expiration: {
maxEntries: 50,
maxAgeSeconds: 300
}
}
}
]
}
}
}
Build Performance
Faster builds:
module.exports = {
parallel: require('os').cpus().length > 1,
configureWebpack: {
optimization: {
splitChunks: {
chunks: 'all',
cacheGroups: {
vendor: {
test: /[\\/]node_modules[\\/]/,
name: 'vendors',
priority: -10
}
}
}
}
}
}
Our Final Configuration
vue.config.js:
const CompressionPlugin = require('compression-webpack-plugin')
module.exports = {
productionSourceMap: false,
configureWebpack: {
plugins: [
new CompressionPlugin({
algorithm: 'gzip',
test: /\.(js|css)$/,
threshold: 10240
})
],
optimization: {
splitChunks: {
chunks: 'all',
cacheGroups: {
vendor: {
test: /[\\/]node_modules[\\/]/,
name: 'vendors',
priority: -10
}
}
}
},
externals: {
vue: 'Vue',
'vue-router': 'VueRouter',
vuex: 'Vuex'
}
},
chainWebpack: config => {
config.plugins.delete('prefetch')
config.module
.rule('images')
.use('image-webpack-loader')
.loader('image-webpack-loader')
.options({
mozjpeg: { quality: 65 },
pngquant: { quality: [0.65, 0.90] }
})
}
}
Results
Before:
- Bundle size: 5.2MB
- Load time (3G): 15 seconds
- Lighthouse score: 45
After:
- Bundle size: 200KB (96% reduction)
- Load time (3G): 2 seconds (87% faster)
- Lighthouse score: 95
Business impact:
- Conversion rate: +23%
- Bounce rate: -35%
- User satisfaction: +40%
Lessons Learned
- Analyze first - Know what’s in your bundle
- Code split by route - Biggest impact
- Replace heavy dependencies - moment.js → day.js
- Use CDN - For common libraries
- Compress everything - gzip is free performance
Conclusion
Bundle size directly impacts user experience and business metrics. Optimize your builds.
Key takeaways:
- Code splitting by route
- Tree shaking and dead code elimination
- Replace heavy dependencies
- Compress with gzip
- Use CDN for common libraries
Every kilobyte matters. Optimize your Vue.js builds today.