Vue3+Vite Build Optimization: 7 Key Strategies from Bundle Size to Loading Performance
Your Vue3 Project Builds to 3.8MB, First Paint Takes 8 Seconds
You built a mid-to-back office system with Vue3+Vite. Development hot-reload is blazing fast, but once deployed: the bundle is 3.8MB, first contentful paint takes 8 seconds, and Lighthouse performance scores 42. You tried various optimization techniques—Tree Shaking doesn't seem to work, code splitting makes things slower, and CDN externalization throws errors everywhere. In 2026, Vue3+Vite build optimization has matured into a well-established best practice system. From Rollup configuration to code splitting, from Tree Shaking to CDN externalization, 7 key strategies can reduce your bundle size by 70% and first paint time by 60%.
This article starts from Vite's underlying build principles and walks you through 7 key optimization strategies with complete hands-on examples, from bundle size to loading performance, covering the full pipeline from development to production.
Vue3+Vite Build Optimization Core Concepts
| Concept | Description |
|---|---|
| Tree Shaking | Optimization technique that removes unused code based on ESM static analysis |
| Code Splitting | Splitting code by route or feature into multiple chunks for on-demand loading |
| Lazy Loading | Route-level or component-level lazy loading, loading only necessary code for first paint |
| CDN Externalization | Importing large libraries via CDN instead of bundling them |
| Compression | gzip/brotli compression of build output to reduce transfer size |
| Source Map | Source mapping files for production debugging, requires strategic handling |
| Rollup | The bundler used by Vite under the hood for production builds |
| manualChunks | Rollup option for manually configuring code splitting strategies |
| sideEffects | package.json field marking whether modules have side effects, affects Tree Shaking |
Vite Build Pipeline
Development Mode:
Source → esbuild pre-bundle deps → Browser ESM on-demand loading → Instant HMR
Production Build:
Source → Rollup parse → Tree Shaking → Code Splitting → Minification → Output
Problem Analysis: 5 Major Challenges in Vue3+Vite Build Optimization
- Uncontrolled Bundle Size: Third-party dependencies not imported on-demand, duplicate bundling, and unconfigured Tree Shaking cause continuously growing output size
- Tree Shaking Not Working: Using CommonJS modules, missing sideEffects config, and dynamic imports causing static analysis failures
- Unclear Code Splitting Strategy: Poor manualChunks configuration leading to chunk fragmentation or shared dependency duplication
- CDN Externalization Configuration Errors: Misconfigured vite-plugin-cdn-import, CDN resource loading failures, version mismatches
- Source Map Leaking in Production: Accidentally uploading source maps to CDN, exposing source code information and creating security risks
Step-by-Step: 7 Key Optimization Strategies
Strategy 1: Vite Base Configuration Optimization
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import { resolve } from 'path'
export default defineConfig({
plugins: [vue()],
resolve: {
alias: {
'@': resolve(__dirname, 'src'),
},
},
build: {
target: 'es2020',
outDir: 'dist',
assetsDir: 'assets',
minify: 'terser',
terserOptions: {
compress: {
drop_console: true,
drop_debugger: true,
pure_funcs: ['console.log', 'console.info'],
},
},
cssCodeSplit: true,
rollupOptions: {
input: {
main: resolve(__dirname, 'index.html'),
},
output: {
chunkFileNames: 'assets/js/[name]-[hash].js',
entryFileNames: 'assets/js/[name]-[hash].js',
assetFileNames: 'assets/[ext]/[name]-[hash].[ext]',
compact: true,
},
},
reportCompressedSize: false,
chunkSizeWarningLimit: 500,
},
})
Strategy 2: Code Splitting Strategy
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import { resolve } from 'path'
export default defineConfig({
plugins: [vue()],
build: {
rollupOptions: {
output: {
manualChunks(id) {
if (id.includes('node_modules')) {
if (id.includes('vue') || id.includes('@vue')) {
return 'vendor-vue'
}
if (id.includes('element-plus')) {
return 'vendor-element'
}
if (id.includes('echarts')) {
return 'vendor-echarts'
}
if (id.includes('lodash')) {
return 'vendor-lodash'
}
if (id.includes('axios')) {
return 'vendor-axios'
}
return 'vendor-other'
}
},
},
},
},
})
Route-level code splitting:
import { createRouter, createWebHistory, type RouteRecordRaw } from 'vue-router'
const routes: RouteRecordRaw[] = [
{
path: '/',
name: 'Home',
component: () => import('@/views/Home.vue'),
},
{
path: '/dashboard',
name: 'Dashboard',
component: () => import('@/views/Dashboard.vue'),
},
{
path: '/user',
name: 'User',
component: () => import('@/views/user/UserLayout.vue'),
children: [
{
path: 'profile',
name: 'UserProfile',
component: () => import('@/views/user/Profile.vue'),
},
{
path: 'settings',
name: 'UserSettings',
component: () => import('@/views/user/Settings.vue'),
},
],
},
{
path: '/admin',
name: 'Admin',
component: () => import('@/views/admin/AdminLayout.vue'),
children: [
{
path: 'users',
name: 'AdminUsers',
component: () => import('@/views/admin/UserManage.vue'),
},
{
path: 'roles',
name: 'AdminRoles',
component: () => import('@/views/admin/RoleManage.vue'),
},
],
},
]
const router = createRouter({
history: createWebHistory(),
routes,
})
export default router
Strategy 3: Tree Shaking Deep Optimization
package.json configuration:
{
"name": "vue3-vite-app",
"version": "1.0.0",
"sideEffects": [
"*.css",
"*.scss",
"*.less",
"*.vue"
]
}
On-demand import for Element Plus:
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import AutoImport from 'unplugin-auto-import/vite'
import Components from 'unplugin-vue-components/vite'
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
export default defineConfig({
plugins: [
vue(),
AutoImport({
resolvers: [ElementPlusResolver()],
dts: 'src/auto-imports.d.ts',
}),
Components({
resolvers: [ElementPlusResolver()],
dts: 'src/components.d.ts',
}),
],
})
On-demand import for lodash:
import { debounce, throttle, cloneDeep, merge } from 'lodash-es'
const handleSearch = debounce((keyword: string) => {
console.log('searching:', keyword)
}, 300)
const handleScroll = throttle(() => {
console.log('scrolling')
}, 100)
const clonedData = cloneDeep(originalData)
const mergedConfig = merge(defaultConfig, userConfig)
Strategy 4: Route and Component Lazy Loading
Component-level lazy loading:
<template>
<div class="page-container">
<h1>Dashboard</h1>
<div class="chart-section">
<LazyChart v-if="showChart" :data="chartData" />
</div>
<div class="table-section">
<LazyTable v-if="showTable" :rows="tableData" />
</div>
</div>
</template>
<script setup lang="ts">
import { ref, defineAsyncComponent } from 'vue'
const LazyChart = defineAsyncComponent(() =>
import('@/components/Chart.vue')
)
const LazyTable = defineAsyncComponent(() =>
import('@/components/DataTable.vue')
)
const showChart = ref(false)
const showTable = ref(false)
const chartData = ref([])
const tableData = ref([])
setTimeout(() => {
showChart.value = true
}, 300)
setTimeout(() => {
showTable.value = true
}, 600)
</script>
Suspense with lazy loading:
<template>
<Suspense>
<template #default>
<LazyDashboard />
</template>
<template #fallback>
<div class="loading-skeleton">
<div class="skeleton-line" />
<div class="skeleton-line" />
<div class="skeleton-line short" />
</div>
</template>
</Suspense>
</template>
<script setup lang="ts">
import { defineAsyncComponent } from 'vue'
const LazyDashboard = defineAsyncComponent(() =>
import('@/views/Dashboard.vue')
)
</script>
Strategy 5: CDN Externalization
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import cdn from 'vite-plugin-cdn-import'
export default defineConfig({
plugins: [
vue(),
cdn({
modules: [
{
name: 'vue',
var: 'Vue',
path: 'https://unpkg.com/vue@3.5.13/dist/vue.global.prod.js',
},
{
name: 'vue-router',
var: 'VueRouter',
path: 'https://unpkg.com/vue-router@4.4.5/dist/vue-router.global.prod.js',
},
{
name: 'pinia',
var: 'Pinia',
path: 'https://unpkg.com/pinia@2.2.6/dist/pinia.iife.prod.js',
},
{
name: 'axios',
var: 'Axios',
path: 'https://unpkg.com/axios@1.7.7/dist/axios.min.js',
},
{
name: 'echarts',
var: 'echarts',
path: 'https://unpkg.com/echarts@5.5.1/dist/echarts.min.js',
},
],
}),
],
})
Custom CDN domain configuration:
import { defineConfig, type Plugin } from 'vite'
import vue from '@vitejs/plugin-vue'
function customCdnPlugin(): Plugin {
const cdnBase = 'https://cdn.yourcompany.com/libs'
const externals: Record<string, string> = {
vue: 'Vue',
'vue-router': 'VueRouter',
pinia: 'Pinia',
axios: 'Axios',
echarts: 'echarts',
}
const versions: Record<string, string> = {
vue: '3.5.13',
'vue-router': '4.4.5',
pinia: '2.2.6',
axios: '1.7.7',
echarts: '5.5.1',
}
return {
name: 'custom-cdn-plugin',
enforce: 'pre',
resolveId(id) {
if (externals[id]) {
return { id: externals[id], external: true }
}
return null
},
transformIndexHtml(html) {
const scripts = Object.entries(externals)
.map(([name, varName]) => {
const version = versions[name]
return `<script src="${cdnBase}/${name}@${version}/index.prod.js"></script>`
})
.join('\n')
return html.replace('</head>', `${scripts}\n</head>`)
},
}
}
export default defineConfig({
plugins: [vue(), customCdnPlugin()],
build: {
rollupOptions: {
external: Object.keys({
vue: 'Vue',
'vue-router': 'VueRouter',
pinia: 'Pinia',
axios: 'Axios',
echarts: 'echarts',
}),
output: {
globals: {
vue: 'Vue',
'vue-router': 'VueRouter',
pinia: 'Pinia',
axios: 'Axios',
echarts: 'echarts',
},
},
},
},
})
Strategy 6: Compression Optimization
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import viteCompression from 'vite-plugin-compression'
export default defineConfig({
plugins: [
vue(),
viteCompression({
algorithm: 'gzip',
ext: '.gz',
threshold: 1024,
deleteOriginFile: false,
}),
viteCompression({
algorithm: 'brotliCompress',
ext: '.br',
threshold: 1024,
deleteOriginFile: false,
}),
],
build: {
cssMinify: 'lightningcss',
},
})
Nginx configuration for gzip:
server {
listen 80;
server_name your-app.com;
gzip on;
gzip_static on;
gzip_vary on;
gzip_proxied any;
gzip_comp_level 6;
gzip_min_length 1024;
gzip_types
text/plain
text/css
text/xml
text/javascript
application/javascript
application/json
application/xml
application/rss+xml
image/svg+xml;
location / {
root /usr/share/nginx/html;
index index.html;
try_files $uri $uri/ /index.html;
}
}
Strategy 7: Source Map Optimization
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
export default defineConfig(({ mode }) => {
const isProduction = mode === 'production'
return {
plugins: [vue()],
build: {
sourcemap: isProduction ? 'hidden' : true,
},
}
})
Source Map upload script:
import { execSync } from 'child_process'
import { readFileSync, readdirSync, statSync } from 'fs'
import { join, extname } from 'path'
import https from 'https'
interface SourceMapUploaderConfig {
distDir: string
release: string
apiEndpoint: string
authToken: string
}
function uploadSourceMaps(config: SourceMapUploaderConfig): void {
const { distDir, release, apiEndpoint, authToken } = config
function findSourceMaps(dir: string): string[] {
const results: string[] = []
const entries = readdirSync(dir)
for (const entry of entries) {
const fullPath = join(dir, entry)
const stat = statSync(fullPath)
if (stat.isDirectory()) {
results.push(...findSourceMaps(fullPath))
} else if (extname(entry) === '.map') {
results.push(fullPath)
}
}
return results
}
const sourceMaps = findSourceMaps(distDir)
console.log(`Found ${sourceMaps.length} source maps`)
for (const mapFile of sourceMaps) {
const content = readFileSync(mapFile, 'utf-8')
const jsFile = mapFile.replace('.map', '')
const data = JSON.stringify({
release,
url: jsFile.replace(distDir, ''),
sourcemap: content,
})
const options = {
hostname: new URL(apiEndpoint).hostname,
path: new URL(apiEndpoint).pathname,
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Content-Length': Buffer.byteLength(data),
Authorization: `Bearer ${authToken}`,
},
}
const req = https.request(options, (res) => {
console.log(`Uploaded ${mapFile}: ${res.statusCode}`)
})
req.on('error', (error) => {
console.error(`Failed to upload ${mapFile}:`, error.message)
})
req.write(data)
req.end()
}
}
const release = execSync('git rev-parse --short HEAD').toString().trim()
uploadSourceMaps({
distDir: './dist',
release,
apiEndpoint: 'https://sentry.io/api/0/projects/your-org/your-project/releases/',
authToken: process.env.SENTRY_AUTH_TOKEN || '',
})
Pitfall Guide: 5 Common Traps
Pitfall 1: CommonJS Modules Break Tree Shaking
❌ Wrong approach:
import _ from 'lodash'
const data = _.cloneDeep(originalData)
✅ Correct approach:
import { cloneDeep } from 'lodash-es'
const data = cloneDeep(originalData)
Pitfall 2: manualChunks Configuration Causes Circular Dependencies
❌ Wrong approach:
manualChunks(id) {
if (id.includes('node_modules')) {
return 'vendor'
}
}
✅ Correct approach:
manualChunks(id) {
if (id.includes('node_modules')) {
if (id.includes('vue') || id.includes('@vue')) {
return 'vendor-vue'
}
if (id.includes('element-plus')) {
return 'vendor-element'
}
return 'vendor-other'
}
}
Pitfall 3: CDN Externalization Breaks Development Environment
❌ Wrong approach:
import cdn from 'vite-plugin-cdn-import'
export default defineConfig({
plugins: [cdn({ modules: [...] })],
})
✅ Correct approach:
import cdn from 'vite-plugin-cdn-import'
export default defineConfig(({ mode }) => ({
plugins: [
mode === 'production' ? cdn({ modules: [...] }) : null,
].filter(Boolean),
}))
Pitfall 4: Dynamic Imports Without Magic Comments
❌ Wrong approach:
component: () => import('@/views/Dashboard.vue')
✅ Correct approach:
component: () => import(
'@/views/Dashboard.vue'
)
With webpackChunkName compatible syntax:
component: () => import(
'@/views/Dashboard.vue'
)
Pitfall 5: Source Maps Deployed Directly to Production
❌ Wrong approach:
export default defineConfig({
build: {
sourcemap: true,
},
})
✅ Correct approach:
export default defineConfig(({ mode }) => ({
build: {
sourcemap: mode === 'production' ? 'hidden' : true,
},
}))
Error Troubleshooting Table
| Error Message | Cause | Solution |
|---|---|---|
Rollup failed to resolve import "xxx" |
Module not properly configured in globals after CDN externalization | Check rollupOptions.output.globals includes the module's global variable name |
Chunk size limit warning: xxx exceeds 500KB |
Single chunk exceeds warning threshold | Optimize manualChunks splitting strategy, or adjust chunkSizeWarningLimit |
Tree Shaking not working for CJS module |
CommonJS modules don't support static analysis | Use lodash-es instead of lodash, confirm dependencies provide ESM versions |
Uncaught TypeError: xxx is not a function |
CDN load failure or version mismatch | Check CDN URL accessibility, confirm global variable name matches CDN export |
Source map error: JSON.parse |
Source map file corrupted or malformed | Rebuild output, check sourcemap configuration |
Dynamic import() not supported |
Build target too low for dynamic imports | Set build.target to 'es2020' or higher |
CSS code split causing FOUC |
CSS code splitting causes flash of unstyled content | Inline critical CSS, or set cssCodeSplit to false |
vite-plugin-compression: brotli not supported |
Node.js version too low for brotli | Upgrade Node.js to v11.7.0+, or use gzip only |
manualChunks circular dependency |
manualChunks splitting causes circular references | Avoid splitting interdependent packages into different chunks, use fine-grained splitting |
CDN script load timeout |
CDN resource loading timeout | Configure CDN fallback to local resources, use self-hosted CDN or preconnect |
Advanced Optimization Techniques
Technique 1: Rollup Plugin Visualizer for Bundle Analysis
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import { visualizer } from 'rollup-plugin-visualizer'
export default defineConfig({
plugins: [
vue(),
visualizer({
filename: './dist/stats.html',
open: true,
gzipSize: true,
brotliSize: true,
template: 'treemap',
}),
],
})
After analysis completes, open dist/stats.html to visually see each module's size ratio and precisely identify bundle bottlenecks.
Technique 2: Preload and Prefetch Strategy
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import { resolve } from 'path'
import type { Plugin } from 'vite'
function prefetchPlugin(): Plugin {
return {
name: 'prefetch-plugin',
transformIndexHtml(html) {
return html.replace(
'</head>',
`<link rel="dns-prefetch" href="//cdn.yourcompany.com">
<link rel="preconnect" href="//cdn.yourcompany.com" crossorigin>
<link rel="prefetch" href="/assets/js/vendor-vue" as="script">
</head>`
)
},
}
}
export default defineConfig({
plugins: [vue(), prefetchPlugin()],
})
Vue Router prefetch:
import { createRouter, createWebHistory, type RouteRecordRaw } from 'vue-router'
const routes: RouteRecordRaw[] = [
{
path: '/',
name: 'Home',
component: () => import('@/views/Home.vue'),
},
{
path: '/dashboard',
name: 'Dashboard',
component: () => import('@/views/Dashboard.vue'),
meta: { prefetch: true },
},
]
const router = createRouter({
history: createWebHistory(),
routes,
})
router.beforeEach((to, _from, next) => {
if (to.meta.prefetch) {
const link = document.createElement('link')
link.rel = 'prefetch'
link.href = to.matched[0]?.components
? '/assets/js/dashboard-chunk.js'
: ''
if (link.href) document.head.appendChild(link)
}
next()
})
export default router
Technique 3: Vite Build Cache and Incremental Builds
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import { resolve } from 'path'
export default defineConfig({
cacheDir: 'node_modules/.vite',
build: {
rollupOptions: {
cache: true,
},
},
optimizeDeps: {
include: [
'vue',
'vue-router',
'pinia',
'axios',
'lodash-es',
'@vueuse/core',
],
exclude: ['@iconify-icons/ep'],
},
})
CI/CD cache configuration:
name: Build and Deploy
on:
push:
branches: [main]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- name: Cache Vite
uses: actions/cache@v4
with:
path: |
node_modules/.vite
node_modules/.cache
key: vite-${{ runner.os }}-${{ hashFiles('package-lock.json') }}
restore-keys: |
vite-${{ runner.os }}-
- name: Install dependencies
run: npm ci
- name: Build
run: npm run build
- name: Upload artifacts
uses: actions/upload-artifact@v4
with:
name: dist
path: dist/
Build Tool Comparison Analysis
| Feature | Vite | Webpack | Rollup | esbuild | Turbopack |
|---|---|---|---|---|---|
| Dev startup speed | Extremely fast (native ESM) | Slow (full bundle) | N/A | Extremely fast | Extremely fast |
| HMR speed | Extremely fast | Slow | N/A | Fast | Extremely fast |
| Production build | Rollup | Webpack | Rollup | Needs integration | SWC |
| Tree Shaking | Excellent (native ESM) | Good | Excellent (native) | Basic | Basic |
| Code splitting | Excellent | Excellent | Good | Not supported | Basic |
| Plugin ecosystem | Rich (Rollup compatible) | Richest | Rich | Limited | Limited |
| Configuration complexity | Low | High | Medium | Low | Low |
| Build cache | Built-in | Requires plugin | Requires plugin | Built-in | Built-in |
| Vue3 support | Officially recommended | Requires loader | Requires plugin | Requires config | Experimental |
| Use case | Modern frontend projects | Complex enterprise apps | Library development | Ultra-fast builds | Next.js |
Summary
Vue3+Vite build optimization is not a one-time task, but a continuous iterative process. From Rollup configuration to code splitting, from Tree Shaking to CDN externalization, the 7 key strategies cover the entire build optimization pipeline. Remember: analyze before optimizing—use rollup-plugin-visualizer to find bottlenecks; split before compressing—proper code splitting is more effective than blind compression; local first, then CDN—ensure CDN externalization doesn't affect development experience. In 2026, the Vite ecosystem is mature enough—leverage these strategies and your project's bundle size can be reduced by 70% and first paint time by 60%.
Recommended Tools
- JSON Formatter - Format build configuration files and debug JSON syntax errors
- Base64 Encoder - Encode Source Maps or inline resources
- Hash Calculator - Calculate file hashes to verify build output integrity
Try these browser-local tools — no sign-up required →