正在加载,请稍候…

Vite Build Optimization: Code Splitting, Tree Shaking, and Production Performance

Master Vite build optimization with code splitting, tree shaking, rollup configuration, and bundle analysis to achieve top Lighthouse scores.

Why Vite Build Performance Matters

Vite's development server is famously fast, but production builds tell a different story. A poorly configured Vite project can produce multi-megabyte bundles that tank your Lighthouse scores and drive users away before your app even loads. In 2026, users abandon pages that take more than 3 seconds to load, and Google's Core Web Vitals directly impact search ranking.

This guide dives deep into Vite's production build pipeline and shows you the specific configurations that move the needle from a 60-point Lighthouse score to 95+.

Understanding Vite's Build Pipeline

Vite uses Rollup under the hood for production builds. Every optimization technique available in Rollup is available in Vite — configured through build.rollupOptions. Vite's dev mode uses esbuild for near-instant transforms; prod mode uses Rollup for deep optimizations. They are fundamentally different pipelines.

Code Splitting Strategies

Manual Chunks — The Highest-Impact Change

The default Vite strategy puts all node_modules content into a single vendor chunk. For a medium-sized app this creates a 500KB+ chunk that must download and parse before anything renders.

// vite.config.ts
import { defineConfig } from 'vite'

export default defineConfig({
  build: {
    rollupOptions: {
      output: {
        manualChunks: {
          // React runtime — changes rarely, cache forever
          'react-vendor': ['react', 'react-dom'],
          // UI component library — heavy but stable
          'ui-vendor': ['@radix-ui/react-dialog', '@radix-ui/react-dropdown-menu'],
          // Routing
          'router': ['react-router-dom'],
          // Utility libraries
          'utils': ['lodash-es', 'date-fns', 'zod'],
        },
      },
    },
  },
})

When you ship separate chunks, a returning user after a minor app update only re-downloads chunks that actually changed. The react-vendor chunk never changes and is served from browser cache instantly.

Function-Based Splitting for Larger Apps

function manualChunks(id: string) {
  if (id.includes('/src/pages/')) {
    const match = id.match(/\/pages\/([^/]+)/)
    if (match) return `page-${match[1]}`
  }
  if (id.includes('node_modules')) {
    if (id.includes('recharts') || id.includes('d3')) return 'charts-vendor'
    if (id.includes('@codemirror') || id.includes('monaco-editor')) return 'editor-vendor'
    if (id.includes('framer-motion')) return 'animation-vendor'
    return 'vendor'
  }
}

export default defineConfig({
  build: { rollupOptions: { output: { manualChunks } } },
})

Route-Based Code Splitting with React.lazy

// App.tsx
import { lazy, Suspense } from 'react'
import { Routes, Route } from 'react-router-dom'

const Dashboard = lazy(() => import('./pages/Dashboard'))
const Settings  = lazy(() => import('./pages/Settings'))
const Analytics = lazy(() => import('./pages/Analytics'))

export function App() {
  return (
    <Suspense fallback={<PageSkeleton />}>
      <Routes>
        <Route path="/" element={<Dashboard />} />
        <Route path="/settings" element={<Settings />} />
        <Route path="/analytics" element={<Analytics />} />
      </Routes>
    </Suspense>
  )
}

Users who never visit /analytics never download 280KB of charting code.

Tree Shaking: Making It Actually Work

Tree shaking eliminates dead code, but only when dependencies use ES modules with side-effect-free exports.

The lodash Problem and Solution

// Bad: imports ENTIRE lodash (~70KB gzipped)
import _ from 'lodash'
const result = _.groupBy(items, 'category')

// Good: lodash-es supports tree shaking
import { groupBy } from 'lodash-es'
const result = groupBy(items, 'category')

Bundle Analysis with rollup-plugin-visualizer

import { visualizer } from 'rollup-plugin-visualizer'

export default defineConfig({
  plugins: [
    visualizer({
      filename: 'dist/bundle-analysis.html',
      gzipSize: true,
      brotliSize: true,
      open: true, // Auto-open after build
    }),
  ],
})

Run vite build and the visualizer opens automatically showing an interactive treemap of your bundle. Every rectangle is a module; size equals bytes. This is how you discover moment.js with full locale data hiding in your date picker.

Rollup Advanced Configuration

Minification and esbuild Options

export default defineConfig({
  build: {
    // esbuild is the default — fast and well-integrated
    minify: 'esbuild',

    // Drop console/debugger statements in production
    esbuildOptions: {
      drop: ['console', 'debugger'],
    },

    // Target modern browsers — less transpilation = smaller output
    target: 'es2020',
  },
})

Asset Configuration

export default defineConfig({
  build: {
    assetsInlineLimit: 4096,  // Inline assets < 4KB as base64
    cssCodeSplit: true,        // Separate CSS per chunk for parallel loading
    sourcemap: 'hidden',       // Source maps for error monitoring, not visible to users

    rollupOptions: {
      output: {
        chunkFileNames:  'assets/[name]-[hash].js',
        entryFileNames:  'assets/[name]-[hash].js',
        assetFileNames:  'assets/[name]-[hash].[ext]',
      },
    },
  },
})

Build Performance: Faster CI Pipelines

export default defineConfig({
  optimizeDeps: {
    include: ['react', 'react-dom', 'react-router-dom'],
    exclude: ['@vite/client', '@vite/env'],
  },
  build: {
    rollupOptions: {
      maxParallelFileOps: 20,
    },
  },
})

Environment-Specific Configuration

import { defineConfig, loadEnv } from 'vite'

export default defineConfig(({ mode }) => {
  const env = loadEnv(mode, process.cwd(), '')

  return {
    define: {
      __APP_VERSION__: JSON.stringify(env.npm_package_version),
      __ENABLE_ANALYTICS__: mode === 'production',
    },
    build: {
      sourcemap: mode === 'staging' ? true : 'hidden',
      chunkSizeWarningLimit: 500, // KB — warn on large chunks
    },
  }
})

Measuring Your Results

# Build and analyze
vite build --mode production

# Serve and run Lighthouse
npm install -g serve
serve dist &
npx lighthouse http://localhost:3000 --view

Key Lighthouse metrics to track:

  • FCP (First Contentful Paint): Target < 1.8s
  • LCP (Largest Contentful Paint): Target < 2.5s
  • TBT (Total Blocking Time): Target < 200ms
  • CLS (Cumulative Layout Shift): Target < 0.1

Real-World Results: Dashboard App Case Study

Metric Before After Change
Bundle size (gzip) 847 KB 312 KB −63%
Chunk count 1 8 +7
LCP 4.2s 1.8s −57%
TBT 680ms 120ms −82%
Lighthouse score 61 94 +33 pts

The biggest single win: moving recharts and d3 to their own chunk and lazy-loading the analytics page.

Summary

Vite build optimization is about understanding how browsers load code. Split vendor dependencies by stability, use dynamic imports for routes users might never visit, verify tree shaking with the visualizer plugin, and measure with Lighthouse after every significant change. Ten minutes of configuration gets you from "adequate" to "fast."