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

  1. Uncontrolled Bundle Size: Third-party dependencies not imported on-demand, duplicate bundling, and unconfigured Tree Shaking cause continuously growing output size
  2. Tree Shaking Not Working: Using CommonJS modules, missing sideEffects config, and dynamic imports causing static analysis failures
  3. Unclear Code Splitting Strategy: Poor manualChunks configuration leading to chunk fragmentation or shared dependency duplication
  4. CDN Externalization Configuration Errors: Misconfigured vite-plugin-cdn-import, CDN resource loading failures, version mismatches
  5. 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%.


Try these browser-local tools — no sign-up required →

#Vue3#Vite#构建优化#打包体积#代码分割#2026#性能调优