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

  1. Analyze first - Know what’s in your bundle
  2. Code split by route - Biggest impact
  3. Replace heavy dependencies - moment.js → day.js
  4. Use CDN - For common libraries
  5. Compress everything - gzip is free performance

Conclusion

Bundle size directly impacts user experience and business metrics. Optimize your builds.

Key takeaways:

  1. Code splitting by route
  2. Tree shaking and dead code elimination
  3. Replace heavy dependencies
  4. Compress with gzip
  5. Use CDN for common libraries

Every kilobyte matters. Optimize your Vue.js builds today.