普通视图

发现新文章,点击刷新页面。
昨天以前首页

Zustand 实战指南:从基础到高级,构建类型安全的状态管理

作者 PanZonghui
2025年10月5日 17:07

在现代 React 应用开发中,状态管理是核心环节之一。相较于 Redux 的繁琐配置和 Context API 的性能局限,Zustand 凭借其简洁的 API 设计、优秀的类型支持和轻量化特性,成为越来越多开发者的首选。本文将以一个「文章列表状态管理」的实战案例为核心,从基础概念到高级特性,全面讲解 Zustand 的使用方法,并与 Redux/RTK 进行对比分析。

一、Zustand 核心优势

在开始实战前,先明确 Zustand 为何能脱颖而出:

  • 极简 API:无需 Provider 包裹根组件,一行代码创建 Store,学习成本极低。
  • 原生 TypeScript 支持:类型推导清晰,无需额外定义大量类型文件,天生类型安全。
  • 中间件生态:官方提供 devtools(Redux 调试)、persist(状态持久化)等实用中间件,开箱即用。
  • 轻量高效:包体积仅约 1KB(gzip 后),无多余依赖,性能损耗可忽略。
  • 灵活的状态更新:支持直接修改状态或函数式更新,满足复杂场景需求。

二、实战准备:环境与依赖

首先确保项目中安装 Zustand(支持 React 16.8+):

# npm
npm install zustand

# yarn
yarn add zustand

# pnpm
pnpm add zustand

若需要使用调试工具或状态持久化,无需额外安装依赖——相关中间件已内置在 zustand/middleware 中。

三、从 0 到 1:构建文章列表 Store

以下将以「文章列表状态管理」为例,分步骤讲解 Zustand 的完整使用流程,涵盖类型定义、状态初始化、异步操作、中间件集成等核心环节。

3.1 第一步:定义 State 与 Actions 类型(类型安全基石)

Zustand 推荐先明确状态(State)和操作(Actions)的类型,这是实现类型安全的关键。通过 TypeScript 约束,可在开发阶段规避状态赋值错误、函数参数不匹配等问题。

// 引入业务相关类型(根据实际项目定义)
import type { Article, QueryParams, PageResult } from '@/types'

// 1. 状态(State)类型:定义需要存储的数据结构
type State = {
  // 核心业务数据
  hasQueryArticleList: boolean; // 是否已发起过列表查询(避免重复初始化)
  articleList: Article[];       // 文章列表数据
  currentPage: number;          // 当前页码(分页控制)
  totalPage: number;            // 总页数(判断是否有更多数据)
  hasMore: boolean;             // 是否还有更多数据(控制“加载更多”按钮)
  scrollTop: number;            // 页面滚动高度(持久化滚动位置)

  // 交互状态(提升用户体验)
  loading: boolean;             // 加载中状态(防重复请求、显示加载动画)
  error: string | null;         // 错误信息(请求失败时显示)
}

// 2. 操作(Actions)类型:定义修改状态的方法
type Actions = {
  // 异步业务操作
  queryArticleList: (queryParams: QueryParams) => Promise<void>; // 初始化查询列表
  loadMore: (queryParams: QueryParams) => Promise<void>;         // 加载更多

  // 基础状态操作
  setScrollTop: (scrollTop: number) => void; // 更新滚动高度
  resetArticleState: () => void;             // 重置所有状态
  clearError: () => void;                    // 清除错误信息
}

3.2 第二步:初始化状态(统一初始值)

为避免状态分散赋值导致的不一致,建议定义一个初始状态对象,后续直接复用:

const initialState: State = {
  // 核心业务数据初始值
  hasQueryArticleList: false,
  articleList: [],
  currentPage: 1,  // 分页默认从第 1 页开始
  totalPage: 0,    // 初始无数据,总页数为 0
  hasMore: false,  // 初始无更多数据
  scrollTop: 0,    // 初始滚动高度为 0

  // 交互状态初始值
  loading: false,  // 初始非加载中
  error: null      // 初始无错误
}

3.3 第三步:实现 Store 核心逻辑(StateCreator)

通过 StateCreator 函数定义状态与操作的具体逻辑,该函数接收两个核心方法:

  • set:用于更新状态,支持对象式(set({ key: value }))和函数式(set(state => ({ key: state.key + 1 })))更新。
  • get:用于获取当前状态(如判断加载中状态、获取当前页码)。
import { create, type StateCreator } from 'zustand'
import { queryArticleList } from '@/api' // 文章列表接口请求函数

// 定义 Store 创建器:整合 State 与 Actions
const storeCreator: StateCreator<State & Actions> = (set, get) => ({
  // 1. 合并初始状态
  ...initialState,

  // 2. 基础操作实现
  /** 重置状态(适用场景:切换筛选条件、清空列表) */
  resetArticleState: () => set(initialState),

  /** 清除错误信息(适用场景:用户关闭错误提示) */
  clearError: () => set({ error: null }),

  /** 更新滚动高度(适用场景:页面滚动事件中调用) */
  setScrollTop: (scrollTop: number) => set({ scrollTop }),

  // 3. 异步操作:初始化查询文章列表(覆盖旧数据)
  queryArticleList: async (queryParams: QueryParams) => {
    // 防重复请求:若正在加载中,直接返回
    const { loading } = get()
    if (loading) return Promise.resolve()

    try {
      // 开启加载中 + 清空旧错误
      set({ loading: true, error: null })

      // 发起接口请求(显式类型断言,确保类型安全)
      const res = await queryArticleList(queryParams)
      const pageData = res.data as PageResult<Article>

      // 请求成功:更新状态(覆盖旧列表、更新分页信息)
      set({
        articleList: pageData.records,
        currentPage: pageData.current,
        totalPage: pageData.pages,
        hasMore: pageData.current < pageData.pages,
        hasQueryArticleList: true,
        error: null
      })
    } catch (err) {
      // 请求失败:格式化错误信息
      const errorMsg = err instanceof Error ? err.message : '查询文章列表失败'
      console.error('【列表查询失败】:', err)
      set({ error: errorMsg })
    } finally {
      // 无论成功/失败,关闭加载中
      set({ loading: false })
    }
  },

  // 4. 异步操作:加载更多(追加数据)
  loadMore: async (queryParams: QueryParams) => {
    // 边界判断:加载中 / 无更多数据时,不发起请求
    const { loading, hasMore, currentPage } = get()
    if (loading || !hasMore) return Promise.resolve()

    try {
      set({ loading: true, error: null })

      // 构造下一页参数:复用筛选条件,页码+1
      const requestParams = { ...queryParams, pageNum: currentPage + 1 }

      // (可选)模拟网络延迟(开发环境测试用)
      await new Promise(resolve => setTimeout(resolve, 500))

      // 发起请求
      const res = await queryArticleList(requestParams)
      const pageData = res.data as PageResult<Article>

      // 请求成功:追加数据(而非覆盖)
      set(state => ({
        articleList: [...state.articleList, ...(pageData.records || [])],
        currentPage: pageData.current,
        totalPage: pageData.pages,
        hasMore: pageData.current < pageData.pages,
        error: null
      }))
    } catch (err) {
      const errorMsg = err instanceof Error ? err.message : '加载更多失败'
      console.error('【加载更多失败】:', err)
      set({ error: errorMsg })
    } finally {
      set({ loading: false })
    }
  }
})

3.4 第四步:集成中间件(调试 + 持久化)

Zustand 中间件可增强 Store 功能,常用的有 devtools(Redux 调试工具支持)和 persist(状态持久化)。通过链式调用整合中间件,最终创建可在组件中使用的 Hook。

import { devtools, persist, createJSONStorage } from 'zustand/middleware'

// 创建并导出 Store Hook
const useArticleStore = create<State & Actions>()(
  // 1. 集成 Redux 调试工具(仅开发环境生效)
  devtools(
    // 2. 集成状态持久化(存储到 localStorage)
    persist(storeCreator, {
      name: 'article-storage', // 持久化存储的 key(localStorage 中可见)
      storage: createJSONStorage(() => localStorage), // 存储方式(支持 localStorage/sessionStorage)
      // 选择性持久化:仅存储需要保留的状态(排除临时状态如 loading/error)
      partialize: state => {
        const { loading, error, ...persistedState } = state
        return persistedState
      }
    }),
    // 调试工具配置
    {
      name: 'ArticleStore', // 调试工具中显示的 Store 名称(多 Store 时便于区分)
      enabled: process.env.NODE_ENV === 'development' // 仅开发环境启用
    }
  )
)

export default useArticleStore

四、在组件中使用 Store

创建好 Store 后,在 React 组件中通过自定义 Hook(如 useArticleStore)获取状态和操作,用法简洁且无需 Provider 包裹。

4.1 基础用法:获取状态与调用操作

import React, { useEffect } from 'react'
import useArticleStore from '@/store/useArticleStore'
import type { QueryParams } from '@/types'

const ArticleListPage: React.FC = () => {
  // 1. 获取状态(推荐使用解构,避免不必要的重渲染)
  const {
    articleList,
    loading,
    error,
    hasMore,
    scrollTop,
    queryArticleList,
    loadMore,
    setScrollTop,
    clearError
  } = useArticleStore()

  // 2. 初始化查询参数
  const defaultQueryParams: QueryParams = {
    pageNum: 1,
    pageSize: 10,
    category: 'tech' // 示例:默认查询“技术”分类
  }

  // 3. 页面首次加载时查询列表
  useEffect(() => {
    queryArticleList(defaultQueryParams)
  }, [queryArticleList, defaultQueryParams])

  // 4. 监听滚动事件,记录滚动高度(用于持久化)
  useEffect(() => {
    const handleScroll = () => {
      const top = window.scrollY
      setScrollTop(top)
    }
    window.addEventListener('scroll', handleScroll)
    // 组件卸载时移除监听
    return () => window.removeEventListener('scroll', handleScroll)
  }, [setScrollTop])

  // 5. 组件挂载时恢复滚动位置(持久化生效)
  useEffect(() => {
    window.scrollTo(0, scrollTop)
  }, [scrollTop])

  // 渲染逻辑...
  return (
    <div className="article-list-page">
      {/* 错误提示 */}
      {error && (
        <div className="error-bar">
          {error}
          <button onClick={clearError}>关闭</button>
        </div>
      )}

      {/* 文章列表 */}
      <div className="article-list">
        {loading && <div>加载中...</div>}
        {!loading && articleList.map(article => (
          <div key={article.id} className="article-item">
            <h3>{article.title}</h3>
            <p>{article.summary}</p>
          </div>
        ))}
      </div>

      {/* 加载更多按钮 */}
      {hasMore && !loading && (
        <button onClick={() => loadMore(defaultQueryParams)}>
          加载更多
        </button>
      )}
    </div>
  )
}

export default ArticleListPage

4.2 性能优化:避免不必要的重渲染

默认情况下,组件会在 Store 中任何状态变化时重渲染。若组件仅依赖部分状态,可通过选择器(Selector) 精确获取所需状态,减少重渲染次数。

方式 1:使用函数式选择器(基础优化)

// 仅获取 articleList 和 loading,仅当这两个状态变化时才重渲染
const articleList = useArticleStore(state => state.articleList)
const loading = useArticleStore(state => state.loading)

方式 2:使用 shallow 比较(复杂对象/数组优化)

若选择器返回对象或数组(如 { articleList, hasMore }),默认会进行引用比较,导致每次状态变化都重渲染。此时可结合 shallow 中间件进行浅比较:

// 1. 引入 shallow 中间件
import { shallow } from 'zustand/shallow'

// 2. 用 shallow 比较对象/数组
const { articleList, hasMore } = useArticleStore(
  state => ({
    articleList: state.articleList,
    hasMore: state.hasMore
  }),
  shallow // 浅比较:仅当 articleList 或 hasMore 本身变化时才重渲染
)

五、Zustand 与 Redux/RTK 的对比分析

选择状态管理库时,了解不同方案的优缺点至关重要。以下从多个维度对比 Zustand 与 Redux(及 Redux Toolkit):

5.1 核心概念与API设计

特性 Zustand Redux (传统) Redux Toolkit (RTK)
核心概念 基于 Hook,无 Provider 包裹 单一 Store、Action、Reducer、Middleware 简化 Redux,整合 createSlice、createAsyncThunk
状态更新 直接通过 set 方法修改 必须通过 dispatch(action) 触发 reducer 通过 createSlice 的 reducers 直接修改(Immer 支持)
异步操作 直接在 Action 中写 async/await 需要额外 middleware(如 redux-thunk) 内置 createAsyncThunk 处理异步
模板代码量 极少(无需定义 action type、action creator) 极多(action type、action creator、reducer 分离) 较少(createSlice 自动生成 action)
类型支持 原生支持,类型推导自然 需手动定义大量类型(action、state、reducer) 类型支持良好,但仍需显式定义部分类型

5.2 代码量对比(以文章列表为例)

Zustand 实现(约 150 行)

  • 直接定义 State + Actions 类型
  • 实现核心逻辑(同步/异步操作)
  • 集成中间件(调试 + 持久化)

Redux Toolkit 实现(约 300 行)

  • 定义 State 类型
  • 创建 Slice(含 reducers 和 extraReducers)
  • 定义异步 thunk(createAsyncThunk)
  • 配置 Store(configureStore)
  • 在根组件添加 Provider
  • 组件中通过 useSelector + useDispatch 使用

5.3 适用场景分析

场景 推荐方案 理由
小型项目/快速原型 Zustand 学习成本低,代码简洁,无需配置
中大型项目 Zustand/RTK Zustand 适合状态分散管理;RTK 适合严格遵循 Flux 架构的团队
团队协作(多人开发) RTK 严格的规范(action-reducer 分离)便于协作,减少代码风格差异
已有 Redux 经验的团队 RTK 平滑过渡,保留团队技术栈熟悉度
对包体积敏感的项目 Zustand 体积仅 1KB,远小于 RTK(约 15KB)
需要与 Redux 生态集成 RTK 可无缝使用 redux-saga、redux-observable 等中间件

5.4 性能对比

  • Zustand:通过选择器(selector)精确控制重渲染,性能优异;无 Context 嵌套问题。
  • Redux:默认使用 Context 传递 Store,深层嵌套组件可能存在性能问题(需配合 memo + 精确 selector 优化)。
  • RTK:通过 createSelector 等工具优化性能,但本质仍依赖 Context,大型应用需额外优化。

六、高级特性与最佳实践

6.1 多 Store 设计

Zustand 支持创建多个独立的 Store(如 useArticleStoreuseUserStore),避免单一 Store 过于臃肿。多 Store 之间可通过 get 方法相互访问:

// 在 UserStore 中访问 ArticleStore 的状态
const useUserStore = create((set, get) => ({
  getUserArticleCount: () => {
    // 获取 ArticleStore 的文章列表
    const articleList = useArticleStore.getState().articleList
    // 获取当前用户 ID
    const userId = get().userId
    // 计算当前用户的文章数量
    return articleList.filter(article => article.authorId === userId).length
  }
}))

6.2 状态持久化进阶

persist 中间件支持更多配置,满足复杂场景需求:

  • blacklist/whitelist:排除/仅保留指定状态(与 partialize 功能类似)。
  • onRehydrateStorage:持久化恢复前的回调(如处理过期数据)。
  • version:版本控制(用于数据迁移)。

示例:处理过期的持久化数据

persist(storeCreator, {
  name: 'article-storage',
  storage: createJSONStorage(() => localStorage),
  // 持久化恢复前触发
  onRehydrateStorage: (state) => {
    // 返回一个回调,接收恢复后的状态
    return (rehydratedState, error) => {
      if (error) {
        console.error('持久化数据恢复失败:', error)
      } else if (rehydratedState) {
        // 假设数据有效期为 1 小时,超过则重置
        const now = Date.now()
        const lastUpdateTime = rehydratedState.lastUpdateTime || 0
        if (now - lastUpdateTime > 3600000) {
          useArticleStore.setState(initialState) // 重置为初始状态
        }
      }
    }
  }
})

6.3 调试工具使用

集成 devtools 后,可在浏览器开发者工具的「Redux」标签中查看:

  • 状态变更历史(每一次 set 操作都会被记录)。
  • 每次变更的前后状态对比。
  • 异步操作的执行流程(如 queryArticleList 的开始/结束)。

调试时可通过 name 字段区分多个 Store,便于定位问题。

七、总结

Zustand 以其「简洁、高效、类型安全」的特性,为 React 状态管理提供了优雅的解决方案。与 Redux/RTK 相比,它更适合追求开发效率、低学习成本的项目,同时在性能和扩展性上也不逊色。

本文通过实战案例,覆盖了 Zustand 的核心用法:

  1. 类型定义:先定义 StateActions 类型,确保类型安全。
  2. Store 创建:通过 StateCreator 实现状态与操作逻辑,支持同步/异步操作。
  3. 中间件集成:利用 devtools 调试、persist 持久化,增强 Store 功能。
  4. 组件使用:通过自定义 Hook 获取状态,结合选择器优化性能。

无论是中小型项目的简单状态管理,还是大型项目的复杂状态拆分,Zustand 都能胜任。建议在实际项目中尝试,并根据业务需求灵活运用其高级特性。

Vite 构建优化实战:从配置到落地的全方位性能提升指南

作者 PanZonghui
2025年10月5日 17:06

在现代前端工程化体系中,构建工具的性能直接影响开发效率与线上应用体验。Vite 作为新一代构建工具,凭借其基于 ES Module 的开发服务器和 Rollup 驱动的生产构建,已成为 React、Vue 等框架的首选工具。然而,随着项目复杂度提升(如引入大量第三方库、多页面架构),默认配置下的 Vite 构建可能出现包体积过大、首屏加载慢、构建时间过长等问题。

本文将以实际 React 项目的 Vite 配置为例,从环境变量处理、分包策略、资源压缩、构建优化四大维度,拆解可落地的 Vite 打包优化方案,帮助开发者实现「更小体积、更快加载、更优体验」的目标。

一、基础配置优化:规范环境与路径

在优化前,需先确保基础配置的合理性——环境变量管理混乱、路径解析复杂会导致后续优化难以落地。以下是两个核心基础优化点:

1. 环境变量安全注入:仅暴露必要变量

Vite 原生支持 .env 环境文件,但默认会将所有变量注入项目,可能导致敏感信息泄露(如未过滤的非 VITE_ 前缀变量)。优化方案是精准筛选变量,仅保留业务所需的 VITE_ 前缀变量,并挂载到 process.env 供项目使用:

define: {
  'process.env': Object.fromEntries(
    // 仅筛选 VITE_ 前缀的环境变量,避免冗余/敏感信息注入
    Object.entries(loadEnv(mode, process.cwd()))
      .filter(([key]) => key.startsWith('VITE_'))
  ),
}
  • 优势:减少注入到代码中的冗余变量,降低构建后包体积;避免非预期变量泄露(如数据库密码、密钥等)。
  • 注意:Vite 规定,只有 VITE_ 前缀的变量会被客户端代码访问,非前缀变量仅在构建脚本中生效,无需注入客户端。

2. 路径别名与扩展名:提升开发效率与构建稳定性

大型项目中,../.. 这类相对路径不仅难维护,还可能导致 Rollup 解析歧义。通过 resolve 配置优化路径处理:

resolve: {
  // 别名:用 @ 代替 src 目录,简化导入路径
  alias: {
    '@': path.resolve(__dirname, 'src'),
  },
  // 明确省略的扩展名,避免 Rollup 猜测导致的解析错误
  extensions: ['.mjs', '.js', '.ts', '.jsx', '.tsx', '.json'],
}
  • 开发侧收益:导入组件时可写 import Button from '@/components/Button',无需计算相对路径层级。
  • 构建侧收益:明确扩展名减少 Rollup 解析时间,避免因同名文件(如 utils.jsutils.ts)导致的打包错误。

二、分包策略:解决「大包阻塞加载」问题

默认情况下,Vite 会将所有第三方依赖打包成一个 vendor.js 文件。当项目依赖较多(如同时引入 React、路由、状态管理、Markdown 渲染库)时,vendor.js 可能超过 1MB,导致首屏加载时「单个大文件阻塞渲染」。

优化核心是按依赖功能拆分代码块,实现「并行加载、按需加载」,以下是可复用的分块方案:

1. 自定义分块逻辑:按功能聚合依赖

通过 build.rollupOptions.output.manualChunks 配置,将第三方依赖按「功能领域」拆分,例如将 React 核心、路由、状态管理、数据请求分别打包:

// 分块策略函数:按依赖功能分组
function createOptimizedChunks(): (id: string) => string | undefined {
  const cache = new Map<string, string>()
  // 按功能定义依赖分组
  const groups = {
    reactCore: new Set(['react', 'react-dom', 'scheduler']), // React 核心
    routing: new Set(['react-router', 'react-router-dom']), // 路由
    store: new Set(['zustand']), // 状态管理
    data: new Set(['axios', 'qs']), // 数据请求
    markdown: new Set(['react-markdown', 'remark-gfm']), // Markdown 渲染
    syntax: new Set(['react-syntax-highlighter']), // 语法高亮
  }

  return (id: string) => {
    if (!id.includes('node_modules')) return // 非第三方依赖不处理
    if (cache.has(id)) return cache.get(id) // 缓存避免重复计算

    // 解析包名(支持 scoped 包,如 @remix-run/router)
    const { fullName } = parsePackageId(id)
    let chunkName: string | undefined

    // 1. 匹配预定义的功能分组
    for (const [group, libs] of Object.entries(groups)) {
      if (libs.has(fullName)) {
        chunkName = `vendor-${group}`
        break
      }
    }

    // 2. 前缀匹配(处理生态类依赖,如 micromark-* 系列)
    if (!chunkName) {
      if (fullName.startsWith('micromark')) chunkName = 'vendor-micromark'
      if (fullName.startsWith('hast')) chunkName = 'vendor-hast'
    }

    // 3. 未匹配依赖归入公共包
    if (!chunkName) chunkName = 'vendor-common'

    cache.set(id, chunkName)
    return chunkName
  }
}

// 辅助函数:解析包名(支持 scoped 包)
function parsePackageId(id: string): { fullName: string } {
  const normalizedPath = id.replace(/\\/g, '/')
  // 匹配 scoped 包(如 @react-router/core)
  const scopedMatch = normalizedPath.match(/node_modules\/(@[^/]+\/[^/]+)(?:\/|$)/)
  // 匹配普通包(如 react)
  const unscopedMatch = normalizedPath.match(/node_modules\/([^/]+)(?:\/|$)/)
  return { fullName: scopedMatch?.[1] || unscopedMatch?.[1] || '' }
}

2. 分块效果与优势

拆分后,第三方依赖会生成多个小文件(如 vendor-reactCore.jsvendor-routing.js),而非单个大文件。带来的核心收益:

  • 并行加载:浏览器可同时加载多个小文件(HTTP/2 支持多路复用),减少首屏加载总时间。
  • 缓存复用:当仅升级某一依赖(如更新 axios)时,仅 vendor-data.js 变化,其他 vendor-* 文件可命中缓存,无需重新下载。
  • 按需加载:若某功能(如 Markdown 渲染)仅在特定页面使用,可配合路由懒加载,实现「用不到不加载」。

3. 分块命名规范:兼顾可读性与缓存

同时优化代码块的命名规则,限制 hash 长度(非入口文件无需完整 hash),便于定位问题:

rollupOptions: {
  output: {
    chunkFileNames: 'js/[name]-[hash:8].js', // 非入口块:短 hash(8位)
    entryFileNames: 'js/[name]-[hash].js',   // 入口块:完整 hash(确保唯一性)
    assetFileNames: 'assets/[name]-[hash][extname]', // 静态资源(图片/字体)
  }
}
  • 短 hash 优势:减少文件名长度,同时保证缓存有效性(8位 hash 碰撞概率极低)。
  • 目录分类:将 JS、静态资源分别放入 js/assets/ 目录,便于服务器配置缓存策略(如对 assets/ 目录设置长期缓存)。

三、资源压缩:极致减小包体积

压缩是前端性能优化的「最后一公里」——通过压缩代码、移除冗余内容,进一步减小文件体积。以下是生产环境必启的三类压缩方案:

1. 代码压缩:Terser 精准移除冗余

Vite 生产环境默认使用 Terser 压缩 JS 代码,通过配置可实现「更精细的优化」,例如选择性移除 console、保留关键注释:

build: {
  minify: isProduction ? 'terser' : false, // 生产环境启用 Terser 压缩
  terserOptions: {
    compress: {
      drop_console: false, // 不全局移除 console(避免误删业务日志)
      // 精准移除无用 console 方法(保留 console.error/console.warn 用于线上报错)
      pure_funcs: isProduction 
        ? ['console.log', 'console.info', 'console.debug'] 
        : [],
      drop_debugger: isProduction, // 生产环境移除 debugger
    },
    format: {
      comments: false, // 移除所有注释(包括版权注释,若需保留可配置 filter)
    },
  }
}
  • 关键优化点:不全局移除 console,而是通过 pure_funcs 仅移除调试用的 console.log,保留 console.error 用于线上问题排查。
  • 体积收益:移除 console 和冗余代码后,JS 文件体积可减少 10%-20%。

2. 静态资源压缩:Brotli + Gzip 双方案

静态资源(JS/CSS/HTML)可通过 Gzip 或 Brotli 压缩,进一步减小传输体积。其中 Brotli 压缩率比 Gzip 高 15%-20%,但兼容性稍弱(现代浏览器均支持,IE 不支持)。

通过 vite-plugin-compression 插件,可在构建时自动生成压缩文件(如 app.js.brapp.js.gz),配合服务器配置(Nginx/Apache)实现「按需返回压缩文件」:

import viteCompression from 'vite-plugin-compression'

plugins: [
  isProduction &&
    viteCompression({
      algorithm: 'brotliCompress', // Brotli 压缩
      ext: '.br',
      threshold: 10240, // 仅压缩大于 10KB 的文件(小文件压缩收益低)
      deleteOriginFile: false, // 保留源文件(兼容性降级用)
    }),
  isProduction &&
    viteCompression({
      algorithm: 'gzip', // Gzip 压缩(兼容旧浏览器)
      ext: '.br',
      threshold: 10240,
      deleteOriginFile: false,
    }),
].filter(Boolean)

服务器配置示例(Nginx)

需在 Nginx 中配置「根据浏览器 Accept-Encoding 头返回对应压缩文件」:

http {
  # 启用 gzip 和 brotli 压缩
  gzip on;
  gzip_types text/javascript text/css text/html;
  gzip_vary on;

  brotli on;
  brotli_types text/javascript text/css text/html;
  brotli_vary on;

  # 静态资源缓存
  location ~* \.(js|css|png)$ {
    root /path/to/your/dist;
    expires 30d; # 长期缓存(配合 hash 文件名)
  }
}
  • 体积收益:JS/CSS 文件经 Brotli 压缩后,体积可减少 40%-60%(例如 1MB 的 JS 文件压缩后约 400KB)。

3. 构建分析:可视化定位大文件

优化前需先「找到问题」——通过 rollup-plugin-visualizer 插件生成构建分析报告,直观查看哪些依赖/文件体积过大:

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

plugins: [
  isProduction &&
    visualizer({
      open: true, // 构建完成后自动打开报告
      gzipSize: true, // 显示 Gzip 压缩后体积
      brotliSize: true, // 显示 Brotli 压缩后体积
      filename: 'report.html', // 报告文件路径
      template: 'treemap', // 图表类型(树状图,便于定位大文件)
    }),
].filter(Boolean)

分析报告的核心用途

  • 识别「体积异常的依赖」:例如某冷门库体积占比过高,可替换为轻量替代品(如用 date-fns 代替 moment.js)。
  • 定位「重复打包的代码」:例如某工具函数被多个模块重复引入,可提取为公共模块。
  • 验证优化效果:每次优化后对比报告,确认体积是否下降。

四、其他关键优化:细节决定体验

除上述核心优化外,以下细节配置同样影响构建性能与应用体验:

1. 开发服务器优化:支持局域网访问

在团队协作或真机调试时,需让其他设备访问本地开发服务,通过 server.host 配置实现:

server: {
  host: '0.0.0.0', // 允许局域网设备访问
  port: 4000, // 固定端口,避免每次启动随机端口
}
  • 使用场景:手机连接同一 WiFi 后,通过「电脑 IP:4000」访问开发环境,调试移动端适配问题。

2. 构建警告阈值:提前规避大文件

设置 chunkSizeWarningLimit,当代码块体积超过阈值时输出警告,提前发现潜在的加载性能问题:

build: {
  chunkSizeWarningLimit: 1024, // 1MB,超过则输出警告
}
  • 作用:避免线上出现「单个文件超过 2MB」的情况,倒逼开发者在构建阶段优化分块策略。

3. Tree-Shaking 优化:保留入口签名

Rollup 的 Tree-Shaking 依赖「模块导出签名的稳定性」,通过 preserveEntrySignatures: 'strict' 确保组件库按需导入时能被正确优化:

rollupOptions: {
  preserveEntrySignatures: 'strict', // 严格保留入口模块的导出签名
}
  • 适用场景:当项目使用组件库(如 Ant Design、Material UI)并开启按需导入时,该配置可确保未使用的组件不被打包,减少体积。

总结:Vite 优化的核心思路

Vite 打包优化并非「堆砌配置」,而是围绕「减小体积、加快加载、提升稳定性」三个核心目标,按以下步骤落地:

  1. 定位问题:用 rollup-plugin-visualizer 分析包体积,找到大依赖、重复代码。
  2. 拆分代码:按功能拆分第三方依赖,实现并行加载与缓存复用。
  3. 极致压缩:启用 Terser + Brotli/Gzip,减小传输体积。
  4. 细节优化:配置路径别名、环境变量、构建警告,提升开发效率与线上稳定性。

通过本文的配置方案,可满足中大型 React 项目的构建需求。实际优化时,需结合项目具体依赖(如是否使用 Vue、是否引入大型图表库)调整分块策略与压缩配置,最终实现「构建快、体积小、加载快」的目标。

❌
❌