正在加载,请稍候…

Webpack and Vite Build Optimization: Code Splitting, Tree Shaking, and Bundle Analysis

Deep dive into build tool optimization: Webpack code splitting strategies, tree shaking configuration, chunk naming, Vite build optimization, analyzing bundle size, and reducing initial load time.

Build Optimization: The Multiplier Effect

Every kilobyte you remove from your JavaScript bundle has a multiplier effect: smaller download, faster parse, faster execution. On mobile networks, the difference between a 200KB and 600KB bundle can be the difference between a user staying or leaving.

This guide covers the techniques that matter most, with concrete before/after examples.

Understanding JavaScript Bundle Costs

JavaScript is the most expensive resource type: every byte must be downloaded, decompressed, parsed, compiled, and executed. Images are expensive to download but cheap to parse. JS is expensive at every step.

100KB of JavaScript (gzipped, ~30KB) costs:
1. Network: 30KB download
2. Decompress: CPU + memory
3. Parse: ~80ms on mid-tier phone
4. Compile: ~120ms on mid-tier phone
5. Execute: depends on code complexity

100KB of images: only the download cost

The benchmark: < 200KB gzipped JS for initial load is a reasonable target for most applications.

Webpack Code Splitting

Code splitting divides your bundle into chunks loaded on demand:

// webpack.config.js
module.exports = {
  entry: {
    main: './src/index.ts',
  },
  
  optimization: {
    splitChunks: {
      chunks: 'all',      // Split async AND sync chunks
      cacheGroups: {
        // Vendor chunk: libraries that rarely change
        vendor: {
          test: /[\/]node_modules[\/]/,
          name: 'vendors',
          chunks: 'all',
          priority: 10,
        },
        
        // React-specific chunk
        react: {
          test: /[\/]node_modules[\/](react|react-dom|react-router)[\/]/,
          name: 'react-vendor',
          chunks: 'all',
          priority: 20,   // Higher priority = takes precedence
        },
        
        // Large, infrequently-used libraries
        charts: {
          test: /[\/]node_modules[\/](chart.js|recharts|d3)[\/]/,
          name: 'charts-vendor',
          chunks: 'async', // Only when dynamically imported
          priority: 20,
        },
        
        // Common code shared between multiple entry points
        common: {
          name: 'common',
          minChunks: 2,      // Used in at least 2 chunks
          priority: 5,
          reuseExistingChunk: true,
        },
      },
    },
    
    // Content hash for long-term caching
    chunkIds: 'deterministic',
    moduleIds: 'deterministic',
  },
  
  output: {
    filename: '[name].[contenthash].js',       // main.abc123.js
    chunkFilename: '[name].[contenthash].js',  // vendors.def456.js
  },
}

Dynamic Imports: Route-Based Splitting

// React Router with route-level code splitting
import { lazy, Suspense } from 'react'

// Each route becomes a separate chunk
const Home = lazy(() => import('./pages/Home'))
const Dashboard = lazy(() => 
  import(/* webpackChunkName: "dashboard" */ './pages/Dashboard')
)
const AdminPanel = lazy(() => 
  import(/* webpackChunkName: "admin" */ './pages/AdminPanel')
)

// Preload on hover (reduces perceived loading time)
function NavLink({ to, prefetch, children }) {
  const handleMouseEnter = () => {
    if (prefetch) {
      // Webpack magic comment: starts downloading without rendering
      import(/* webpackPrefetch: true */ `./pages/${to}`)
    }
  }
  
  return (
    <Link to={to} onMouseEnter={handleMouseEnter}>
      {children}
    </Link>
  )
}

function App() {
  return (
    <Suspense fallback={<LoadingSpinner />}>
      <Routes>
        <Route path="/" element={<Home />} />
        <Route path="/dashboard" element={<Dashboard />} />
        <Route path="/admin" element={<AdminPanel />} />
      </Routes>
    </Suspense>
  )
}

Tree Shaking: Remove Dead Code

Tree shaking eliminates unused exports. But it requires proper configuration:

// package.json — signal that your code is side-effect free
{
  "sideEffects": false  // Everything can be tree-shaken
  // OR specify files with side effects:
  "sideEffects": [
    "*.css",
    "src/polyfills.js"
  ]
}

// webpack.config.js — tree shaking only works in production mode
module.exports = {
  mode: 'production',      // Enables TerserPlugin (minification) + tree shaking
  optimization: {
    usedExports: true,     // Mark used exports
    minimizer: [
      new TerserPlugin({
        terserOptions: {
          compress: {
            dead_code: true,    // Remove dead code
            pure_funcs: ['console.log'],  // Remove specific function calls
          },
        },
      }),
    ],
  },
}

Common Tree Shaking Failures

// ❌ Default export prevents tree shaking of utilities
// utils.js
export default {
  formatDate,
  formatCurrency,
  formatBytes,
}

// ❌ Import entire module:
import utils from './utils'
utils.formatDate(date) // formatCurrency and formatBytes still bundled!

// ✅ Named exports enable tree shaking
// utils.js
export function formatDate(date) { /* ... */ }
export function formatCurrency(amount) { /* ... */ }
export function formatBytes(bytes) { /* ... */ }

// ✅ Import only what you need
import { formatDate } from './utils'
// formatCurrency and formatBytes are tree-shaken away

// ❌ CommonJS (require) is not tree-shakeable
const { formatDate } = require('./utils')  // Entire module is included

// ✅ ES modules (import) are tree-shakeable
import { formatDate } from './utils'

Bundle Analysis

# Webpack Bundle Analyzer
npm install --save-dev webpack-bundle-analyzer

# In webpack.config.js
const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer')

module.exports = {
  plugins: [
    process.env.ANALYZE && new BundleAnalyzerPlugin({
      analyzerMode: 'static',     // Generates HTML report
      openAnalyzer: true,
      reportFilename: 'bundle-report.html',
    })
  ].filter(Boolean),
}

# Run:
ANALYZE=true webpack build
# For Vite:
npm install --save-dev rollup-plugin-visualizer

# vite.config.ts
import { visualizer } from 'rollup-plugin-visualizer'

export default defineConfig({
  plugins: [
    visualizer({
      filename: 'stats.html',
      open: true,
      gzipSize: true,
      brotliSize: true,
    })
  ],
})

# Source Map Explorer (works with any bundler)
npm install --save-dev source-map-explorer
npx source-map-explorer dist/main.*.js

What to look for in the analyzer:

  1. Large libraries you didn't know were included (e.g., lodash included fully when you only use one function)
  2. Duplicate modules (same library bundled multiple times)
  3. Dev-only code in production bundle
  4. Unexpectedly large vendor chunks

Vite Build Optimization

// vite.config.ts
import { defineConfig, splitVendorChunkPlugin } from 'vite'
import react from '@vitejs/plugin-react'

export default defineConfig({
  plugins: [
    react(),
    splitVendorChunkPlugin(),  // Splits vendor/app chunks automatically
  ],
  
  build: {
    target: 'es2020',           // Modern browsers — smaller output
    minify: 'esbuild',          // Fast minification (esbuild) or thorough (terser)
    sourcemap: true,            // For debugging (don't deploy to public)
    
    rollupOptions: {
      output: {
        // Manual chunk splitting
        manualChunks: (id) => {
          if (id.includes('node_modules')) {
            // React ecosystem together
            if (id.includes('react') || id.includes('react-dom')) {
              return 'react'
            }
            // Heavy libraries as their own chunk
            if (id.includes('monaco-editor')) return 'monaco'
            if (id.includes('chart.js') || id.includes('recharts')) return 'charts'
            if (id.includes('@tanstack')) return 'tanstack'
            
            // Everything else: vendors
            return 'vendors'
          }
        },
        
        // Asset naming with content hash
        entryFileNames: 'assets/[name]-[hash].js',
        chunkFileNames: 'assets/[name]-[hash].js',
        assetFileNames: 'assets/[name]-[hash].[ext]',
      },
    },
    
    // Warn if chunk exceeds this size
    chunkSizeWarningLimit: 500,  // KB
  },
  
  // Dependency pre-bundling
  optimizeDeps: {
    include: ['react', 'react-dom'],      // Pre-bundle for faster dev
    exclude: ['@vite/client', '@vite/env'], // Don't pre-bundle
  },
})

Compression and Asset Optimization

// Gzip/Brotli compression in webpack
const CompressionPlugin = require('compression-webpack-plugin')
const zlib = require('zlib')

plugins: [
  new CompressionPlugin({
    algorithm: 'gzip',
    test: /.(js|css|html|svg)$/,
    threshold: 10240,    // Only compress files > 10KB
    minRatio: 0.8,       // Only compress if 20%+ savings
  }),
  new CompressionPlugin({
    algorithm: 'brotliCompress',
    test: /.(js|css|html|svg)$/,
    compressionOptions: { params: { [zlib.constants.BROTLI_PARAM_QUALITY]: 11 } },
    filename: '[path][base].br',
    deleteOriginalAssets: false,
  }),
]

// Nginx: serve pre-compressed files
// nginx.conf
// gzip_static on;
// brotli_static on;

Measuring Build Performance

# Webpack build time analysis
npx webpack --profile --json > stats.json
# Upload to https://webpack.github.io/analyse/

# Which loaders/plugins are slow?
npm install --save-dev speed-measure-webpack-plugin

const SpeedMeasurePlugin = require('speed-measure-webpack-plugin')
const smp = new SpeedMeasurePlugin()
module.exports = smp.wrap(webpackConfig)

# Vite build timing
vite build --reporter=verbose

# Track bundle size over time (CI)
npx bundlewatch --config .bundlewatchrc.js
# Fails CI if bundle exceeds defined thresholds
// .bundlewatchrc.js
{
  "files": [
    { "path": "dist/assets/main-*.js", "maxSize": "200kB" },
    { "path": "dist/assets/vendors-*.js", "maxSize": "300kB" },
    { "path": "dist/assets/*.css", "maxSize": "50kB" }
  ],
  "ci": {
    "trackBranches": ["main"],
    "repoBranchIsBaseline": true
  }
}

Build optimization is an investment that pays dividends on every page load for the life of the application. The combination of route-based code splitting, tree shaking, vendor chunk separation, and compression typically reduces initial bundle size by 50-70% compared to unoptimized builds.

→ Convert numbers between binary, octal, decimal, and hexadecimal with the Integer Base Converter.