mcpField
V
vite-bundle-optimization

vite-bundle-optimization

github.com/patternsdev开发257669 次安装28 周前更新
代码仓库

关于 vite-bundle-optimization

面向 Vite + React 开发者的构建优化参考,适用于生产配置以及排查构建缓慢或包体积过大。内容涵盖导入方式、代码拆分、资源加载和 Vite 构建配置。

主要功能

  1. 01避免使用聚合导出导入
  2. 02配置手动代码分块
  3. 03通过动态导入拆分路由
  4. 04延迟加载首屏以下组件
  5. 05延后加载第三方脚本
  6. 06分析构建包内容

适用场景

  1. 01减少初始 JavaScript 负载
  2. 02排查意外膨胀的构建包
  3. 03改善 Vite 开发服务器启动
  4. 04审查生产构建配置

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/patternsdev/skills --skill vite-bundle-optimization

安装后在 Claude Code 中输入/vite-bundle-optimization确认已加载。

详细介绍

Vite 打包优化

目录

适用于生产环境的模式,用于优化 Vite + React 应用的打包体积和构建性能。这些模式利用 Vite 的架构(开发时使用原生 ESM,生产环境使用 Rollup),实现更小、更快的打包产物。

适用场景

在以下情况下参考这些模式:

  • 为生产环境搭建新的 Vite + React 项目
  • 使用 npx vite-bundle-visualizer 分析打包体积
  • 构建时间较长或打包产物异常庞大
  • 从 webpack/CRA 迁移到 Vite
  • 优化 Core Web Vitals(LCP、FID/INP、CLS)

说明

  • 在项目设置、构建配置和打包体积审查期间应用这些模式。发现打包产物庞大或构建缓慢时,使用 npx vite-bundle-visualizer 进行诊断,并应用相应模式。

详情

概述

Vite 使用 esbuild 进行依赖预打包和开发转换,并使用 Rollup 进行生产构建。理解这种双重架构是有效优化的关键。以下模式按影响程度排序。


1. 避免从 Barrel 文件导入

影响:关键 — 可能导致启动时间增加 200-800ms,开发服务器启动时间增加 2-4s。

Barrel 文件(从多个模块重新导出的 index.ts)会迫使打包工具加载整个模块图,即使你只使用了一个导出项。这是 React 应用中最常见的打包体积问题。

避免 — 通过 Barrel 文件导入整个库:

import { Button, TextField } from '@/components'
// Loads ALL components in the barrel, even unused ones

import { Check, X, Menu } from 'lucide-react'
// Loads all 1,500+ icons (~2.8s in dev)

推荐 — 直接导入:

import { Button } from '@/components/Button'
import { TextField } from '@/components/TextField'

import Check from 'lucide-react/dist/esm/icons/check'
import X from 'lucide-react/dist/esm/icons/x'
import Menu from 'lucide-react/dist/esm/icons/menu'

使用 vite-plugin-barrel 自动修复:

// vite.config.ts
import barrel from 'vite-plugin-barrel'

export default defineConfig({
  plugins: [
    react(),
    barrel({
      packages: ['lucide-react', '@mui/material', '@mui/icons-material'],
    }),
  ],
})

该插件会在构建时将 Barrel 导入转换为直接导入,让你既能使用简洁的语法,又能获得直接导入的性能。

常受影响的库: lucide-react、@mui/material、@mui/icons-material、@tabler/icons-react、react-icons、@radix-ui/react-*、lodash、date-fns、rxjs。


2. 配置手动分块

影响:高 — 改善缓存和并行加载,并减小初始打包体积。

Vite 默认会将所有供应商代码放入一个文件。对代码进行拆分,这样频繁变化的应用代码就不会使供应商缓存失效。

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

export default defineConfig({
  plugins: [react()],
  build: {
    rollupOptions: {
      output: {
        manualChunks: {
          // Core React — rarely changes
          'vendor-react': ['react', 'react-dom'],
          // Router — changes infrequently
          'vendor-router': ['react-router-dom'],
          // Data layer — changes occasionally
          'vendor-query': ['@tanstack/react-query'],
          // UI framework — changes with design updates
          'vendor-ui': ['@radix-ui/react-dialog', '@radix-ui/react-dropdown-menu'],
        },
      },
    },
  },
})

如需根据模块路径进行更动态的拆分:

manualChunks(id) {
  if (id.includes('node_modules')) {
    if (id.includes('react-dom')) return 'vendor-react'
    if (id.includes('react-router')) return 'vendor-router'
    if (id.includes('@tanstack')) return 'vendor-query'
    return 'vendor' // everything else
  }
},

3. 使用动态导入进行路由级代码拆分

影响:高 — 仅加载当前页面所需的代码。

使用 React.lazy() 和动态导入,将每个路由拆分到单独的代码块中。

import { lazy, Suspense } from 'react'
import { BrowserRouter, Routes, Route } from 'react-router-dom'

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

function App() {
  return (
    <BrowserRouter>
      <Suspense fallback={<PageSkeleton />}>
        <Routes>
          <Route path="/" element={<Home />} />
          <Route path="/dashboard" element={<Dashboard />} />
          <Route path="/settings" element={<Settings />} />
        </Routes>
      </Suspense>
    </BrowserRouter>
  )
}

Vite 会自动为每个延迟导入创建单独的代码块。为其命名以便调试:

const Dashboard = lazy(() =>
  import(/* webpackChunkName: "dashboard" */ './pages/Dashboard')
)

4. 延迟加载首屏以下的重型组件

影响:高 — 减小初始打包体积,加快 LCP。

首次加载时不可见的组件(模态框、图表、编辑器、地图)应采用延迟加载。

import { lazy, Suspense, useState } from 'react'

const RichTextEditor = lazy(() => import('./components/RichTextEditor'))
const ChartPanel = lazy(() => import('./components/ChartPanel'))

function ArticlePage() {
  const [editing, setEditing] = useState(false)

  return (
    <article>
      <h1>Article Title</h1>
      <p>Content visible immediately...</p>

      {editing && (
        <Suspense fallback={<EditorSkeleton />}>
          <RichTextEditor />
        </Suspense>
      )}

      <Suspense fallback={<ChartSkeleton />}>
        <ChartPanel />
      </Suspense>
    </article>
  )
}

5. 延迟加载第三方脚本

影响:高 — 分析、跟踪和小组件不应阻塞渲染。

在页面可交互后加载非关键第三方脚本。

避免 — 阻塞初始渲染:

// main.tsx
import * as Sentry from '@sentry/react'
import posthog from 'posthog-js'

Sentry.init({ dsn: '...' })
posthog.init('...')

推荐 — 在 hydration/mount 后加载:

// main.tsx — defer to idle time
function initThirdParty() {
  import('@sentry/react').then(Sentry => {
    Sentry.init({ dsn: import.meta.env.VITE_SENTRY_DSN })
  })
  import('posthog-js').then(({ default: posthog }) => {
    posthog.init(import.meta.env.VITE_POSTHOG_KEY)
  })
}

if ('requestIdleCallback' in window) {
  requestIdleCallback(initThirdParty)
} else {
  setTimeout(initThirdParty, 2000)
}

对于外部脚本标签,使用 defer 或动态注入:

function loadScript(src: string) {
  const script = document.createElement('script')
  script.src = src
  script.async = true
  document.body.appendChild(script)
}

6. 根据用户意图预加载关键资源

影响:中 — 消除导航时的感知延迟。

当用户表示出意图(悬停、聚焦)时,就开始加载路由代码,而不是等到点击后再加载。

function NavLink({ to, children }: { to: string; children: React.ReactNode }) {
  const preload = () => {
    // Vite creates a module preload for dynamic imports
    switch (to) {
      case '/dashboard':
        import('./pages/Dashboard')
        break
      case '/settings':
        import('./pages/Settings')
        break
    }
  }

  return (
    <Link to={to} onMouseEnter={preload} onFocus={preload}>
      {children}
    </Link>
  )
}

如需在 HTML head 中使用 <link rel="modulepreload">:


<link rel="modulepreload" href="/assets/Home-abc123.js" />

Vite 会自动为入口代码块添加 <link rel="modulepreload">。对于确定用户接下来会访问的路由,可手动添加预加载。


7. 配置依赖预打包

影响:中 — 加快开发服务器启动和页面加载。

Vite 使用 esbuild 对 node_modules 中的依赖进行预打包。配置预打包以处理边缘情况。

// vite.config.ts
export default defineConfig({
  optimizeDeps: {
    // Force pre-bundle these (useful for CJS deps or deep imports)
    include: [
      'react',
      'react-dom',
      'react-router-dom',
      '@tanstack/react-query',
      'date-fns/format',
      'date-fns/parseISO',
    ],
    // Skip pre-bundling for these (already ESM, or causes issues)
    exclude: ['@vite-pwa/assets-generator'],
  },
})

如果开发环境中有许多小请求,导致页面加载缓慢,通常是因为某个依赖未被预打包。将其添加到 include。


8. 启用压缩

影响:中——传输文件体积可缩小 60%–80%。

Vite 默认不启用压缩。为生产环境添加压缩插件。

// vite.config.ts
import viteCompression from 'vite-plugin-compression'

export default defineConfig({
  plugins: [
    react(),
    viteCompression({ algorithm: 'gzip' }),
    viteCompression({ algorithm: 'brotliCompress' }),
  ],
})

此操作会在资源文件旁生成 .gz 和 .br 文件。配置服务器(Nginx、Cloudflare、Vercel)来提供这些文件。


9. 定期分析打包产物

影响:信息性——在体积回归问题发布前及时发现。

每次依赖项有重大变更后,都运行打包产物可视化工具。

npx vite-bundle-visualizer

也可以将它添加到构建脚本中:

{
  "scripts": {
    "build": "vite build",
    "analyze": "vite build && npx vite-bundle-visualizer"
  }
}

需要关注的事项:

  • 任何单个分块经 gzip 压缩后大于 200KB——考虑拆分
  • 多个分块中加载了重复的库
  • 仅使用少数几个函数,却加载了整个库
  • 可以动态导入的 node_modules 代码

10. 使用 import.meta.env 消除无用代码

影响:低至中——移除生产环境中未使用的代码路径。

Vite 会在构建时替换 import.meta.env.*,使 Rollup 能够通过摇树优化移除无用分支。

// This code is completely removed in production
if (import.meta.env.DEV) {
  console.log('Debug info:', data)
  window.__DEBUG_DATA__ = data
}

// Feature flags eliminated at build time
if (import.meta.env.VITE_FEATURE_NEW_DASHBOARD === 'true') {
  // Only included when flag is set
  initNewDashboard()
}

在 .env 文件中定义自定义环境变量:

# .env.production
VITE_FEATURE_NEW_DASHBOARD=true
VITE_API_URL=https://api.example.com

11. 优化图像和静态资源

影响:中——图像通常是体积最大的资源。

在 Vite 中配置资源处理:

// vite.config.ts
export default defineConfig({
  build: {
    assetsInlineLimit: 4096, // Inline assets < 4KB as base64
    rollupOptions: {
      output: {
        assetFileNames: (assetInfo) => {
          // Organize assets by type
          if (/\.(png|jpe?g|gif|svg|webp|avif)$/.test(assetInfo.name ?? '')) {
            return 'images/[name]-[hash][extname]'
          }
          if (/\.(woff2?|ttf|eot)$/.test(assetInfo.name ?? '')) {
            return 'fonts/[name]-[hash][extname]'
          }
          return 'assets/[name]-[hash][extname]'
        },
      },
    },
  },
})

使用 vite-plugin-image-optimizer 自动压缩图像:

import { ViteImageOptimizer } from 'vite-plugin-image-optimizer'

export default defineConfig({
  plugins: [
    react(),
    ViteImageOptimizer({
      png: { quality: 80 },
      jpeg: { quality: 80 },
      webp: { quality: 80 },
    }),
  ],
})

12. 配置开发服务器代理以便开发 API

影响:中——消除 CORS 问题,简化本地开发。

Vite 单页应用通常会与独立的后端 API 通信。配置 server.proxy,在开发期间转发 API 请求,避免 CORS 问题,并匹配生产环境的 URL 模式。

// vite.config.ts
export default defineConfig({
  server: {
    proxy: {
      '/api': {
        target: 'http://localhost:3001',
        changeOrigin: true,
      },
      '/auth': {
        target: 'http://localhost:3001',
        changeOrigin: true,
      },
      // WebSocket support for real-time features
      '/ws': {
        target: 'ws://localhost:3001',
        ws: true,
      },
    },
  },
})

在应用代码中使用相对路径(fetch('/api/users'))——请求会发送到 Vite 开发服务器,再由其代理到后端。在生产环境中,配置反向代理(Nginx、Caddy)执行相同的路由。


13. 使用 vite-plugin-pwa 添加 PWA 支持

影响:中——为 Vite 单页应用提供离线能力、可安装性和缓存资源。

对于需要离线支持或可安装性的单页应用,vite-plugin-pwa 可处理 Service Worker 生成、预缓存和清单创建。

// vite.config.ts
import { VitePWA } from 'vite-plugin-pwa'

export default defineConfig({
  plugins: [
    react(),
    VitePWA({
      registerType: 'autoUpdate',
      includeAssets: ['favicon.svg', 'robots.txt'],
      manifest: {
        name: 'My App',
        short_name: 'App',
        theme_color: '#ffffff',
        icons: [
          { src: '/icon-192.png', sizes: '192x192', type: 'image/png' },
          { src: '/icon-512.png', sizes: '512x512', type: 'image/png' },
        ],
      },
      workbox: {
        globPatterns: ['**/*.{js,css,html,ico,png,svg,woff2}'],
        runtimeCaching: [
          {
            urlPattern: /^https:\/\/api\./,
            handler: 'NetworkFirst',
            options: { cacheName: 'api-cache', expiration: { maxEntries: 50 } },
          },
        ],
      },
    }),
  ],
})

对于需要静默更新的应用,使用 registerType: 'autoUpdate'。使用 registerType: 'prompt' 可向用户显示更新通知。


14. 选择 CSS 策略

影响:中——Vite 支持多种 CSS 方案,且无需配置。

Vite 开箱即支持 CSS Modules、PostCSS 和预处理器。根据需求选择:

CSS Modules——作用域样式、无运行时开销,内置于 Vite:

// Button.module.css → automatically scoped
import styles from './Button.module.css'

function Button({ children }: { children: React.ReactNode }) {
  return <button className={styles.primary}>{children}</button>
}

Tailwind CSS——实用优先,可与 Vite 的 PostCSS 支持配合使用:

// vite.config.ts — no plugin needed, Tailwind uses PostCSS
// Just install tailwindcss and add postcss.config.js

// postcss.config.js
export default {
  plugins: {
    '@tailwindcss/postcss': {},
  },
}

CSS-in-JS 注意事项: styled-components 和 Emotion 等库会增加运行时开销。对于优先考虑性能的 Vite 单页应用,建议使用 CSS Modules 或 Tailwind。如果需要 CSS-in-JS,可考虑 Vanilla Extract 或 Panda CSS 等零运行时方案。


15. 将 React Compiler 配置为 Vite 插件

影响:高——自动记忆化可省去手动使用 useMemo、useCallback 和 React.memo。

React Compiler 会分析组件并自动插入记忆化代码。在 Vite 项目中,将其添加为 Babel 插件:

npm install -D babel-plugin-react-compiler
// vite.config.ts
import react from '@vitejs/plugin-react'

export default defineConfig({
  plugins: [
    react({
      babel: {
        plugins: ['babel-plugin-react-compiler'],
      },
    }),
  ],
})

启用后,可以逐步移除手动添加的 useMemo、useCallback 和 React.memo 调用——编译器会自动处理。启用后运行测试套件,确认行为保持不变。

编译器需要 React 19。它默认选择性启用;如果希望逐步采用,可以在每个文件中使用 'use memo' 指令启用。


来源

内容取自 patterns.dev——面向更广泛的 React Web 工程社区的 Vite 专属优化指南。

评论

支持 Markdown