Jest vs Vitest:现代前端测试框架的全面对比与迁移指南
深入对比Jest和Vitest的性能、功能、生态系统,提供完整的迁移方案和最佳实践,帮助团队选择最适合的测试框架。
深入对比Jest和Vitest的性能、功能、生态系统,提供完整的迁移方案和最佳实践,帮助团队选择最适合的测试框架。
引言 随着前端应用的复杂度不断增加,传统的单体前端架构已难以满足大型项目的需求。现代前端架构需要解决代码分割、团队协作、性能优化、可维护性等多方面的挑战。本文将从架构设计的角度,深入探讨如何构建可扩展的大型前端应用。 一、前端架构演进史 1.1 从页面到应用的发展历程 jQuery时代 → AMD/RequireJS → CommonJS/Node.js → ES6 Modules → 现代框架时代 发展阶段 阶段 特点 代表技术 局限性 静态页面 HTML + CSS + 少量JS jQuery、Prototype 代码复用差 模块化初期 RequireJS、SeaJS AMD、CMD 加载复杂 组件化 React、Vue诞生 Virtual DOM 状态管理混乱 工程化 Webpack、Rollup ES6 Modules 构建复杂度高 现代化 Next.js、Nuxt.js SSR、SSR Streaming 学习曲线陡峭 1.2 当前架构挑战 大型前端应用面临的核心问题 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 // 问题1: 代码组织混乱 // src/ // components/ // 1000+ 组件 // pages/ // 200+ 页面 // utils/ // 工具函数散落各处 // api/ // API调用不规范 // 问题2: 状态管理复杂 // - 全局状态与局部状态边界模糊 // - 状态流转难以追踪 // - 多个状态管理方案混用 // 问题3: 性能瓶颈 // - 首屏加载时间长 // - 路由切换卡顿 // - 内存泄漏风险 // 问题4: 团队协作困难 // - 代码冲突频繁 // - 发布周期长 // - 技术栈不统一 二、架构设计原则 2.1 SOLID原则在前端的应用 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 // S - 单一职责原则 // ❌ 违反单一职责 class UserComponent { render() { } fetchUser() { } validateInput() { } formatData() { } logAnalytics() { } } // ✅ 遵循单一职责 class UserComponent { constructor( private renderer: UserRenderer, private userService: UserService, private validator: InputValidator ) { } render() { this.renderer.render() } fetchUser() { this.userService.fetch() } } // O - 开闭原则 // 使用插件系统扩展功能 interface ComponentPlugin { install(component: any): void uninstall(component: any): void } class Component { private plugins: ComponentPlugin[] = [] use(plugin: ComponentPlugin) { this.plugins.push(plugin) plugin.install(this) return this } } // L - 里氏替换原则 // 基类可被子类无缝替换 abstract class BaseStore<T> { abstract get(id: string): Promise<T> abstract set(id: string, data: T): Promise<void> } class UserStore extends BaseStore<User> { async get(id: string): Promise<User> { } async set(id: string, data: User): Promise<void> { } } // I - 接口隔离原则 // 拆分大接口为小接口 interface ReadOnlyRepository<T> { findById(id: string): Promise<T> findAll(): Promise<T[]> } interface WriteOnlyRepository<T> { create(data: T): Promise<T> update(id: string, data: Partial<T>): Promise<T> delete(id: string): Promise<void> } // D - 依赖倒置原则 // 依赖抽象而非具体实现 interface CacheService { get(key: string): Promise<any> set(key: string, value: any): Promise<void> } class UserService { constructor(private cache: CacheService) { } } 2.2 分层架构 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 // 标准前端分层架构 ┌─────────────────────────────────────────────┐ │ Presentation Layer │ │ (Components, Pages, UI Logic) │ ├─────────────────────────────────────────────┤ │ Business Logic Layer │ │ (Hooks, Composables, Use Cases) │ ├─────────────────────────────────────────────┤ │ Data Access Layer │ │ (API Client, Cache, State Management) │ ├─────────────────────────────────────────────┤ │ Infrastructure Layer │ │ (HTTP, WebSocket, LocalStorage) │ └─────────────────────────────────────────────┘ // 实现示例 // ========== Presentation Layer ========== // views/UserList.vue <template> <UserDataTable :users="users" :loading="loading" @refresh="handleRefresh" /> </template> // ========== Business Logic Layer ========== // composables/useUserList.ts export function useUserList() { const { users, loading, error, fetch } = userRepository() const refresh = async () => { await fetch() analytics.track('user_list_refreshed') } const filteredUsers = computed(() => { return users.value.filter(user => user.active) }) return { users: filteredUsers, loading, error, refresh } } // ========== Data Access Layer ========== // repositories/userRepository.ts export function userRepository() { const api = useApiClient() const cache = useCache() const fetch = async () => { const cached = await cache.get('users') if (cached) return cached const data = await api.get('/users') await cache.set('users', data, 300) // 5分钟 return data } return { users: ref([]), loading: ref(false), error: ref(null), fetch } } // ========== Infrastructure Layer =========== // utils/apiClient.ts class ApiClient { private baseURL: string private interceptors: RequestInterceptor[] async get<T>(url: string): Promise<T> { const request = new Request(this.baseURL + url) return this.execute<T>(request) } } 2.3 领域驱动设计(DDD)在前端 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 // 1. 领域模型定义 // domains/user/User.ts export class User { constructor( private readonly id: UserId, private readonly email: Email, private name: UserName, private status: UserStatus ) {} // 领域逻辑 activate(): void { if (this.status === UserStatus.ACTIVE) { throw new Error('User already active') } this.status = UserStatus.ACTIVE } deactivate(): void { this.status = UserStatus.INACTIVE } isActive(): boolean { return this.status === UserStatus.ACTIVE } } // 值对象 export class Email { constructor(private readonly value: string) { if (!this.validate(value)) { throw new Error('Invalid email') } } private validate(email: string): boolean { return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email) } getValue(): string { return this.value } } // 2. 领域服务 // domains/user/UserService.ts export class UserService { constructor( private userRepo: UserRepository, private emailService: EmailService ) {} async registerUser(data: RegisterUserData): Promise<User> { // 业务规则验证 if (await this.userRepo.existsByEmail(data.email)) { throw new Error('Email already registered') } // 创建领域对象 const user = new User( UserId.generate(), new Email(data.email), new UserName(data.name), UserStatus.PENDING ) // 持久化 await this.userRepo.save(user) // 发送欢迎邮件 await this.emailService.sendWelcome(user.email.getValue()) return user } } // 3. 应用服务(用例) // application/useCases/RegisterUserUseCase.ts export class RegisterUserUseCase { constructor(private userService: UserService) { } async execute(request: RegisterUserRequest): Promise<RegisterUserResponse> { try { const user = await this.userService.registerUser(request.data) return { success: true, userId: user.id.getValue(), message: 'Registration successful' } } catch (error) { return { success: false, error: error.message } } } } // 4. 在组件中使用 // components/RegisterForm.vue export default { setup() { const registerUser = useRegisterUserUseCase() const handleSubmit = async (formData) => { const result = await registerUser.execute({ data: { email: formData.email, name: formData.name, password: formData.password } }) if (result.success) { router.push(`/welcome/${result.userId}`) } else { showError(result.error) } } return { handleSubmit } } } 三、微前端架构 3.1 微前端架构模式 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 // 模式1: 路由分发 // 主应用根据路由加载不同子应用 const microApps = [ { name: 'user-center', entry: '//localhost:3001', container: '#subapp', activeRule: '/users' }, { name: 'order-system', entry: '//localhost:3002', container: '#subapp', activeRule: '/orders' } ] // 模式2: 组合式集成 // 主应用提供容器,子应用作为组件渲染 <template> <div> <AppHeader /> <MicroApp name="user-center" :props="userProps" /> <AppFooter /> </div> </template> // 模式3: EMP (EMP Module Federation) // 去中心化的模块共享 // 子应用A new ModuleFederationPlugin({ name: 'appA', exposes: { './UserList': './src/components/UserList' }, shared: ['vue', 'vue-router'] }) // 子应用B使用A的组件 import UserList from 'appA/UserList' 3.2 模块联邦实现 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 // webpack.config.js - 主应用 const ModuleFederationPlugin = require('webpack/lib/container/ModuleFederationPlugin') module.exports = { plugins: [ new ModuleFederationPlugin({ name: 'host_app', remotes: { userApp: 'user_app@http://localhost:3001/remoteEntry.js', orderApp: 'order_app@http://localhost:3002/remoteEntry.js' }, shared: { vue: { singleton: true, requiredVersion: '^3.0.0' }, 'vue-router': { singleton: true } } }) ] } // 在主应用中使用远程模块 // src/App.vue <script> import { defineAsyncComponent } from 'vue' // 动态加载远程组件 const UserDashboard = defineAsyncComponent(() => import('userApp/Dashboard') ) const OrderList = defineAsyncComponent(() => import('orderApp/OrderList') ) export default { components: { UserDashboard, OrderList } } </script> // 子应用配置 - userApp // webpack.config.js module.exports = { plugins: [ new ModuleFederationPlugin({ name: 'user_app', filename: 'remoteEntry.js', exposes: { './Dashboard': './src/components/Dashboard.vue', './UserProfile': './src/components/UserProfile.vue', './UserStore': './src/stores/user' }, shared: { vue: { singleton: true, requiredVersion: '^3.0.0' } } }) ] } 3.3 微前端通信方案 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 // ========== 方案1: 自定义事件总线 ========== // utils/eventBus.ts class MicroEventBus { private events: Map<string, Set<Function>> constructor() { this.events = new Map() } on(event: string, callback: Function) { if (!this.events.has(event)) { this.events.set(event, new Set()) } this.events.get(event)!.add(callback) } off(event: string, callback: Function) { this.events.get(event)?.delete(callback) } emit(event: string, data?: any) { this.events.get(event)?.forEach(callback => callback(data)) } } // 全局单例 export const microEventBus = new MicroEventBus() // 子应用A - 发送事件 import { microEventBus } from './eventBus' microEventBus.emit('user:login', { userId: '123', userName: 'Alice' }) // 子应用B - 监听事件 microEventBus.on('user:login', (user) => { console.log('User logged in:', user) updateUI(user) }) // ========== 方案2: 状态共享 ========== // stores/sharedStore.ts import { reactive, readonly } from 'vue' const state = reactive({ user: null, theme: 'light', locale: 'zh-CN' }) export const sharedStore = { state: readonly(state), setUser(user: any) { state.user = user }, setTheme(theme: string) { state.theme = theme }, setLocale(locale: string) { state.locale = locale } } // ========== 方案3: 全局状态管理 ========== // 使用Pinia创建跨应用store // stores/global.ts import { defineStore } from 'pinia' export const useGlobalStore = defineStore('global', { state: () => ({ currentUser: null, notifications: [] }), actions: { async login(credentials) { const user = await api.login(credentials) this.currentUser = user }, logout() { this.currentUser = null }, addNotification(notification) { this.notifications.push(notification) } } }) 3.4 样式隔离 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 // ========== 方案1: CSS Modules ========== // components/UserCard.module.css .card { padding: 20px; border-radius: 8px; background: white; } .title { font-size: 18px; color: #333; } // components/UserCard.vue <template> <div :class="$style.card"> <h3 :class="$style.title">{{ title }}</h3> </div> </template> <script> import styles from './UserCard.module.css' export default { $style: styles } </script> // ========== 方案2: Shadow DOM ========== // components/ShadowWidget.ts class ShadowWidget extends HTMLElement { constructor() { super() // 创建Shadow DOM const shadow = this.attachShadow({ mode: 'open' }) // 隔离的样式 const style = document.createElement('style') style.textContent = ` .widget { padding: 20px; background: #f5f5f5; } .widget h3 { color: #333; margin: 0 0 10px 0; } ` // 隔离的内容 const wrapper = document.createElement('div') wrapper.className = 'widget' wrapper.innerHTML = ` <h3>Shadow DOM Widget</h3> <p>This is isolated from host page styles</p> ` shadow.appendChild(style) shadow.appendChild(wrapper) } } customElements.define('shadow-widget', ShadowWidget) // ========== 方案3: CSS-in-JS ========== // 使用styled-components或emotion import styled from 'styled-components' const Card = styled.div` padding: 20px; border-radius: 8px; background: white; h3 { font-size: 18px; color: #333; } ` // 组件自动生成唯一类名 <Card> <h3>Title</h3> </Card> // ========== 方案4: 命名空间 ========== // 推荐的BEM + 命名空间组合 // [子应用名]-[块]__[元素]--[修饰器] .user-center-card__header--active { background: #007bff; } .order-system-list__item--highlighted { background: #ffc107; } 四、状态管理架构 4.1 状态管理选型 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 // ========== 方案对比 ========== // 1. Redux Toolkit - 复杂应用 import { createSlice, configureStore } from '@reduxjs/toolkit' const userSlice = createSlice({ name: 'user', initialState: { user: null, status: 'idle' }, reducers: { setUser(state, action) { state.user = action.payload }, clearUser(state) { state.user = null } }, extraReducers: (builder) => { builder .addCase(fetchUser.pending, (state) => { state.status = 'loading' }) .addCase(fetchUser.fulfilled, (state, action) => { state.status = 'succeeded' state.user = action.payload }) } }) export const { setUser, clearUser } = userSlice.actions export default configureStore({ reducer: { user: userSlice.reducer } }) // 2. Zustand - 轻量级 import create from 'zustand' const useUserStore = create((set, get) => ({ user: null, status: 'idle', setUser: (user) => set({ user }), fetchUser: async (id) => { set({ status: 'loading' }) const user = await api.getUser(id) set({ user, status: 'succeeded' }) }, getUserAge: () => { const { user } = get() return user ? calculateAge(user.birthday) : 0 } })) // 3. Pinia - Vue推荐 import { defineStore } from 'pinia' export const useUserStore = defineStore('user', { state: () => ({ user: null, status: 'idle' }), getters: { isLoggedIn: (state) => !!state.user, userName: (state) => state.user?.name || 'Guest' }, actions: { async fetchUser(id) { this.status = 'loading' this.user = await api.getUser(id) this.status = 'succeeded' } } }) // 4. Jotai - 原子化状态 import { atom, useAtom } from 'jotai' // 原子状态 const userAtom = atom(null) const statusAtom = atom('idle') // 派生原子 const isLoggedInAtom = atom( (get) => !!get(userAtom) ) // 组件中使用 function UserProfile() { const [user, setUser] = useAtom(userAtom) const [status] = useAtom(statusAtom) const [isLoggedIn] = useAtom(isLoggedInAtom) // ... } 4.2 分层状态架构 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 // ========== 状态分层设计 ========== // 1. 服务端状态 - React Query / SWR import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query' // 获取数据 function useUser(id: string) { return useQuery({ queryKey: ['user', id], queryFn: () => api.getUser(id), staleTime: 5 * 60 * 1000, // 5分钟 cacheTime: 10 * 60 * 1000 // 10分钟 }) } // 修改数据 function useUpdateUser() { const queryClient = useQueryClient() return useMutation({ mutationFn: ({ id, data }) => api.updateUser(id, data), onSuccess: (data, variables) => { // 自动更新缓存 queryClient.setQueryData(['user', variables.id], data) // 或使缓存失效 queryClient.invalidateQueries(['user', variables.id]) } }) } // 2. 全局UI状态 - Zustand import create from 'zustand/vanilla' const uiStore = create((set) => ({ theme: 'light', sidebarOpen: true, modalOpen: false, toggleTheme: () => set((state) => ({ theme: state.theme === 'light' ? 'dark' : 'light' })), toggleSidebar: () => set((state) => ({ sidebarOpen: !state.sidebarOpen })), openModal: () => set({ modalOpen: true }), closeModal: () => set({ modalOpen: false }) })) // 3. 表单状态 - 原子状态 import { atom, useAtom } from 'jotai' const formDataAtom = atom({ username: '', email: '', password: '' }) const formErrorsAtom = atom({}) const useForm = () => { const [data, setData] = useAtom(formDataAtom) const [errors, setErrors] = useAtom(formErrorsAtom) const updateField = (field, value) => { setData((prev) => ({ ...prev, [field]: value })) } const validate = () => { const newErrors = {} if (!data.username) newErrors.username = 'Required' if (!data.email) newErrors.email = 'Required' setErrors(newErrors) return Object.keys(newErrors).length === 0 } return { data, errors, updateField, validate } } // ========== 状态组合使用 ========== function UserProfile({ userId }) { // 服务端状态 const { data: user, isLoading } = useUser(userId) const updateUser = useUpdateUser() // UI状态 const { theme, toggleTheme } = useUiStore() // 本地状态 const [isEditing, setIsEditing] = useState(false) if (isLoading) return <Spinner /> return ( <div className={theme}> {isEditing ? ( <EditForm user={user} onSave={(data) => updateUser.mutate({ id: userId, data })} onCancel={() => setIsEditing(false)} /> ) : ( <UserDetail user={user} onEdit={() => setIsEditing(true)} /> )} <button onClick={toggleTheme}>Toggle Theme</button> </div> ) } 4.3 状态持久化 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 // ========== 本地持久化方案 ========== // 1. Zustand + localStorage import { persist } from 'zustand/middleware' export const useUserStore = create( persist( (set) => ({ user: null, token: null, setUser: (user) => set({ user }), setToken: (token) => set({ token }), logout: () => set({ user: null, token: null }) }), { name: 'user-storage', // 部分持久化 partialize: (state) => ({ user: state.user, token: state.token }) } ) ) // 2. Pinia + localStorage import { createPinia } from 'pinia' import piniaPluginPersistedstate from 'pinia-plugin-persistedstate' const pinia = createPinia() pinia.use(piniaPluginPersistedstate) export const useAuthStore = defineStore('auth', { state: () => ({ token: null, refreshToken: null }), actions: { setTokens(token, refreshToken) { this.token = token this.refreshToken = refreshToken } }, persist: { key: 'auth', storage: localStorage, pick: ['token', 'refreshToken'] } }) // 3. IndexedDB - 大量数据 import { openDB } from 'idb' const dbPromise = openDB('app-db', 1, { upgrade(db) { db.createObjectStore('cache') } }) export const idbStorage = { async getItem(key) { const db = await dbPromise return await db.get('cache', key) }, async setItem(key, value) { const db = await dbPromise return await db.put('cache', value, key) }, async removeItem(key) { const db = await dbPromise return await db.delete('cache', key) } } // 4. 服务端状态同步 import { useQuery, useMutation } from '@tanstack/react-query' // 自动同步到服务端 function useSyncedState(key, initialValue) { const queryClient = useQueryClient() const { data } = useQuery({ queryKey: ['state', key], queryFn: () => api.getState(key), initialData: initialValue }) const mutation = useMutation({ mutationFn: (value) => api.setState(key, value), onSuccess: () => { queryClient.invalidateQueries(['state', key]) } }) const setState = (value) => { mutation.mutate(value) } return [data, setState] } 五、性能优化架构 5.1 代码分割策略 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 // ========== 路由级别分割 ========== // Vue Router const routes = [ { path: '/dashboard', component: () => import( /* webpackChunkName: "dashboard" */ './views/Dashboard.vue' ) }, { path: '/users', component: () => import( /* webpackChunkName: "users" */ './views/Users.vue' ) } ] // React Router const Dashboard = lazy(() => import('./views/Dashboard')) const Users = lazy(() => import('./views/Users')) function App() { return ( <Suspense fallback={<Spinner />}> <Routes> <Route path="/dashboard" element={<Dashboard />} /> <Route path="/users" element={<Users />} /> </Routes> </Suspense> ) } // ========== 组件级别分割 ========== // Vue 3 - defineAsyncComponent const HeavyComponent = defineAsyncComponent({ loader: () => import('./HeavyComponent.vue'), loadingComponent: LoadingSpinner, errorComponent: ErrorDisplay, delay: 200, timeout: 5000 }) // React - lazy + Suspense const HeavyChart = lazy(() => import('./HeavyChart')) function Dashboard() { const [showChart, setShowChart] = useState(false) return ( <div> <button onClick={() => setShowChart(true)}> Show Chart </button> {showChart && ( <Suspense fallback={<Spinner />}> <HeavyChart /> </Suspense> )} </div> ) } // ========== 条件加载 ========== // 只在需要时加载库 const loadMonaco = async () => { const monaco = await import('monaco-editor') return monaco } const loadChartLibrary = async () => { if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) { return null // 用户不喜欢动画 } const chart = await import('echarts') return chart } // ========== 预加载策略 ========== // 预加载下一个可能访问的页面 function ProductList({ products }) { const prefetch = usePrefetch() const handleMouseEnter = (productId) => { // 鼠标悬停时预加载 prefetch(`/products/${productId}`) } return ( <ul> {products.map(product => ( <li key={product.id} onMouseEnter={() => handleMouseEnter(product.id)} > <Link to={`/products/${product.id}`}> {product.name} </Link> </li> ))} </ul> ) } // Webpack魔法注释 const ProductDetail = lazy(() => import( /* webpackPrefetch: true */ /* webpackChunkName: "product-detail" */ './ProductDetail' )) 5.2 渲染优化 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 // ========== 虚拟列表 ========== import { useVirtualizer } from '@tanstack/react-virtual' function VirtualList({ items }) { const parentRef = useRef() const virtualizer = useVirtualizer({ count: items.length, getScrollElement: () => parentRef.current, estimateSize: () => 50, // 每项高度 overscan: 5 // 额外渲染项数 }) return ( <div ref={parentRef} style={{ height: '400px', overflow: 'auto' }}> <div style={{ height: `${virtualizer.getTotalSize()}px`, position: 'relative' }} > {virtualizer.getVirtualItems().map(virtualItem => ( <div key={virtualItem.key} style={{ position: 'absolute', top: 0, left: 0, width: '100%', height: `${virtualItem.size}px`, transform: `translateY(${virtualItem.start}px)` }} > {items[virtualItem.index].content} </div> ))} </div> </div> ) } // ========== 防抖/节流 ========== import { useDebouncedCallback, useThrottledCallback } from '@hooks' function SearchInput() { // 防抖搜索 const debouncedSearch = useDebouncedCallback( (query) => { performSearch(query) }, 500 // 延迟 ) return ( <input type="text" onChange={(e) => debouncedSearch(e.target.value)} /> ) } function ScrollHandler() { // 节流滚动事件 const throttledScroll = useThrottledCallback( () => { updatePosition() }, 100 // 间隔 ) useEffect(() => { window.addEventListener('scroll', throttledScroll) return () => window.removeEventListener('scroll', throttledScroll) }, [throttledScroll]) } // ========== React优化 ========== import { memo, useMemo, useCallback, useRef } from 'react' // 1. 组件记忆化 const ExpensiveComponent = memo(function ExpensiveComponent({ data }) { return <div>{/* 复杂渲染 */}</div> }) // 2. 计算缓存 function DataProcessor({ items }) { const sorted = useMemo(() => { return items.sort((a, b) => a.value - b.value) }, [items]) // 只在items变化时重新计算 const filtered = useMemo(() => { return sorted.filter(item => item.active) }, [sorted]) return <List items={filtered} /> } // 3. 函数稳定化 function ParentComponent() { const [count, setCount] = useState(0) // 不稳定的函数引用 const handleClickBad = () => { setCount(count + 1) } // 稳定的函数引用 const handleClickGood = useCallback(() => { setCount(c => c + 1) }, []) // 依赖为空,函数引用不变 return <ChildComponent onClick={handleClickGood} /> } // 4. 列表渲染优化 function TodoList({ todos, onToggle }) { const todoRefs = useRef(new Map()) const handleToggle = useCallback((id) => { // 批量更新 startTransition(() => { onToggle(id) }) }, [onToggle]) return ( <ul> {todos.map(todo => ( <TodoItem key={todo.id} ref={(node) => { if (node) { todoRefs.current.set(todo.id, node) } else { todoRefs.current.delete(todo.id) } }} todo={todo} onToggle={handleToggle} /> ))} </ul> ) } const TodoItem = memo(function TodoItem({ todo, onToggle }) { return ( <li onClick={() => onToggle(todo.id)}> {todo.text} </li> ) }) // ========== Vue优化 ========== // 1. computed缓存 export default { setup() { const items = ref([]) // 计算属性自动缓存 const filtered = computed(() => { return items.value.filter(item => item.active) }) const sorted = computed(() => { return filtered.value.sort((a, b) => a.value - b.value) }) return { sorted } } } // 2. v-once/v-memo <template> <!-- 只渲染一次 --> <Logo v-once /> <!-- 按条件记忆 --> <div v-for="item in list" :key="item.id" v-memo="[item.id, item.selected]"> {{ item.text }} </div> </template> // 3. watchEffect优化 watchEffect((onCleanup) => { const timer = setInterval(() => { updateData() }, 1000) onCleanup(() => { clearInterval(timer) // 清理副作用 }) }) // 4. shallowRef/shallowReactive // 大对象优化 const state = shallowReactive({ // 只顶层是响应式的 items: largeItemsArray }) // 只需要重新赋值时使用 const data = shallowRef(null) 5.3 资源优化 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 // ========== 图片优化 ========== // 1. 响应式图片 <picture> <source media="(min-width: 1024px)" srcSet="image-large.webp" type="image/webp" /> <source media="(min-width: 768px)" srcSet="image-medium.webp" type="image/webp" /> <source srcSet="image-small.webp" type="image/webp" /> <img src="image-fallback.jpg" alt="Description" loading="lazy" /> </picture> // 2. 懒加载图片 import { lazyload } from '@utils/lazyload' function ImageWithLazy({ src, alt }) { const imgRef = useRef() const [isLoaded, setIsLoaded] = useState(false) useEffect(() => { const observer = new IntersectionObserver( (entries) => { entries.forEach(entry => { if (entry.isIntersecting) { const img = entry.target img.src = img.dataset.src observer.unobserve(img) } }) }, { rootMargin: '50px' } ) if (imgRef.current) { observer.observe(imgRef.current) } return () => observer.disconnect() }, []) return ( <img ref={imgRef} data-src={src} alt={alt} onLoad={() => setIsLoaded(true)} style={{ opacity: isLoaded ? 1 : 0 }} /> ) } // 3. 图片组件封装 function OptimizedImage({ src, alt, width, height, priority = false }) { const [imageSrc, setImageSrc] = useState(null) useEffect(() => { const load = async () => { // 使用WebP转换服务 const webpSrc = await convertToWebP(src, width) setImageSrc(webpSrc) } if (priority) { load() } else { const observer = new IntersectionObserver((entries) => { if (entries[0].isIntersecting) { load() observer.disconnect() } }) // ... observe logic } }, [src, width, priority]) return <img src={imageSrc} alt={alt} loading={!priority ? 'lazy' : 'eager'} /> } // ========== 字体优化 ========== // 1. 字体预加载 <link rel="preload" href="/fonts/main.woff2" as="font" type="font/woff2" crossOrigin="anonymous" /> // 2. 字体显示策略 @font-face { font-family: 'CustomFont'; src: url('/fonts/custom.woff2') format('woff2'); font-display: swap; /* 立即显示后备字体 */ /* font-display: optional; 短暂等待后使用后备字体 */ /* font-display: fallback; 短暂等待,隐藏短时间 */ } // 3. 字体子集化 // 只包含使用的字符 // 使用fonttools或在线工具生成subset // ========== 第三方资源优化 ========== // 动态加载重型库 const loadGoogleMaps = () => { return new Promise((resolve) => { if (window.google) { resolve(window.google) return } const script = document.createElement('script') script.src = `https://maps.googleapis.com/maps/api/js?key=${API_KEY}` script.async = true script.onload = () => resolve(window.google) document.head.appendChild(script) }) } // 使用场景 function MapComponent() { const [map, setMap] = useState(null) useEffect(() => { let mounted = true loadGoogleMaps().then(google => { if (mounted) { setMap(new google.maps.Map(/* ... */)) } }) return () => { mounted = false } }, []) return <div ref={mapRef} /> } // ========== 缓存策略 ========== // Service Worker缓存 // sw.js const CACHE_NAME = 'app-v1' const STATIC_CACHE = [ '/', '/styles/main.css', '/scripts/main.js' ] self.addEventListener('install', (event) => { event.waitUntil( caches.open(CACHE_NAME).then((cache) => { return cache.addAll(STATIC_CACHE) }) ) }) self.addEventListener('fetch', (event) => { event.respondWith( caches.match(event.request).then((response) => { return response || fetch(event.request) }) ) }) // 网络优先策略 async function networkFirst(request) { try { const response = await fetch(request) const cache = await caches.open(CACHE_NAME) cache.put(request, response.clone()) return response } catch { return caches.match(request) } } // 缓存优先策略 async function cacheFirst(request) { const cached = await caches.match(request) if (cached) return cached const response = await fetch(request) const cache = await caches.open(CACHE_NAME) cache.put(request, response.clone()) return response } // 过期策略 async function staleWhileRevalidate(request) { const cache = await caches.open(CACHE_NAME) const cached = await cache.match(request) const fetchPromise = fetch(request).then((response) => { cache.put(request, response.clone()) return response }) return cached || fetchPromise } 六、构建优化 6.1 Webpack优化配置 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 // webpack.config.js const path = require('path') const TerserPlugin = require('terser-webpack-plugin') const CssMinimizerPlugin = require('css-minimizer-webpack-plugin') module.exports = { // ========== 1. 构建模式 ========== mode: 'production', // ========== 2. 代码分割 ========== optimization: { splitChunks: { chunks: 'all', cacheGroups: { // 框架代码单独打包 framework: { test: /[\\/]node_modules[\\/](react|react-dom|vue)[\\/]/, name: 'framework', priority: 40 }, // UI库单独打包 ui: { test: /[\\/]node_modules[\\/](@mui|@ant-design|element-plus)[\\/]/, name: 'ui', priority: 30 }, // 工具库 lib: { test: /[\\/]node_modules[\\/](lodash|axios|dayjs)[\\/]/, name: 'lib', priority: 20 }, // 公共代码 commons: { minChunks: 2, priority: 10, reuseExistingChunk: true } } }, // 运行时代码单独提取 runtimeChunk: 'single', // 压缩配置 minimize: true, minimizer: [ new TerserPlugin({ terserOptions: { parse: { ecma: 8 }, compress: { ecma: 5, warnings: false, comparisons: false, inline: 2, drop_console: true }, mangle: { safari10: true }, output: { ecma: 5, comments: false, ascii_only: true } } }), new CssMinimizerPlugin() ] }, // ========== 3. 模块解析 ========== resolve: { // 路径别名 alias: { '@': path.resolve(__dirname, 'src'), '@components': path.resolve(__dirname, 'src/components'), '@utils': path.resolve(__dirname, 'src/utils') }, // 扩展名 extensions: ['.js', '.jsx', '.ts', '.tsx', '.vue', '.json'], // 模块查找路径 modules: ['node_modules', path.resolve(__dirname, 'src')] }, // ========== 4. 缓存配置 ========== cache: { type: 'filesystem', buildDependencies: { config: [__filename] } }, // ========== 5. 输出配置 ========== output: { path: path.resolve(__dirname, 'dist'), filename: 'js/[name].[contenthash:8].js', chunkFilename: 'js/[name].[contenthash:8].chunk.js', // 资源文件名 assetModuleFilename: 'assets/[name].[hash:8][ext]' }, // ========== 6. 性能优化 ========== performance: { hints: 'warning', maxAssetSize: 244 * 1024, // 244KB maxEntrypointSize: 244 * 1024 } } 6.2 Vite优化配置 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 // vite.config.ts import { defineConfig } from 'vite' import vue from '@vitejs/plugin-vue' import { visualizer } from 'rollup-plugin-visualizer' import viteCompression from 'vite-plugin-compression' export default defineConfig({ plugins: [ vue(), // 构建分析 visualizer({ open: true, gzipSize: true, brotliSize: true }), // Gzip压缩 viteCompression({ algorithm: 'gzip', ext: '.gz' }), // Brotli压缩 viteCompression({ algorithm: 'brotliCompress', ext: '.br' }) ], build: { // 产物目录 outDir: 'dist', // 静态资源目录 assetsDir: 'assets', // 生成source map sourcemap: false, // 构建后是否生成 manifest.json manifest: true, // chunk大小警告阈值 chunkSizeWarningLimit: 1000, // 代码分割 rollupOptions: { output: { // 手动分包 manualChunks: { 'vue-vendor': ['vue', 'vue-router', 'pinia'], 'ui-lib': ['element-plus'], 'utils': ['axios', 'lodash-es', 'dayjs'] }, // 文件命名 chunkFileNames: 'js/[name]-[hash].js', entryFileNames: 'js/[name]-[hash].js', assetFileNames: '[ext]/[name]-[hash].[ext]' } }, // 压缩 minify: 'terser', terserOptions: { compress: { drop_console: true, drop_debugger: true } } }, // 依赖优化 optimizeDeps: { include: ['vue', 'vue-router', 'pinia'], exclude: ['some-large-dep'] }, // CSS配置 css: { modules: { localsConvention: 'camelCase' }, preprocessorOptions: { scss: { additionalData: `@import "@/styles/variables.scss";` } } }, // 开发服务器 server: { port: 3000, open: true, proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true, rewrite: (path) => path.replace(/^\/api/, '') } } } }) 七、监控与调试 7.1 性能监控 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 // ========== Core Web Vitals监控 ========== // utils/performanceMonitor.ts interface PerformanceMetrics { FCP: number // First Contentful Paint LCP: number // Largest Contentful Paint FID: number // First Input Delay CLS: number // Cumulative Layout Shift TTFB: number // Time to First Byte } export class PerformanceMonitor { private metrics: Partial<PerformanceMetrics> = {} async collectMetrics(): Promise<PerformanceMetrics> { // 等待页面加载完成 await new Promise(resolve => window.addEventListener('load', resolve)) // FCP const fcpEntry = performance.getEntriesByName('first-contentful-paint')[0] as any this.metrics.FCP = fcpEntry?.startTime || 0 // LCP this.metrics.LCP = await this.getLCP() // FID this.metrics.FID = await this.getFID() // CLS this.metrics.CLS = await this.getCLS() // TTFB const navigation = performance.getEntriesByType('navigation')[0] as any this.metrics.TTFB = navigation?.responseStart || 0 return this.metrics as PerformanceMetrics } private getLCP(): Promise<number> { return new Promise(resolve => { new PerformanceObserver((list) => { const entries = list.getEntries() const lastEntry = entries[entries.length - 1] as any resolve(lastEntry?.startTime || 0) }).observe({ entryTypes: ['largest-contentful-paint'] }) }) } private getFID(): Promise<number> { return new Promise(resolve => { new PerformanceObserver((list) => { const firstInput = list.getEntries()[0] as any resolve(firstInput?.processingStart - firstInput?.startTime || 0) }).observe({ entryTypes: ['first-input'] }) }) } private getCLS(): Promise<number> { return new Promise(resolve => { let clsValue = 0 new PerformanceObserver((list) => { for (const entry of list.getEntries() as any[]) { if (!entry.hadRecentInput) { clsValue += entry.value } } resolve(clsValue) }).observe({ entryTypes: ['layout-shift'] }) }) } reportToAnalytics(metrics: PerformanceMetrics) { // 发送到分析平台 analytics.track('page_performance', { fcp: metrics.FCP, lcp: metrics.LCP, fid: metrics.FID, cls: metrics.CLS, ttfb: metrics.TTFB, url: window.location.href, userAgent: navigator.userAgent }) } } // ========== 错误监控 ========== // utils/errorTracker.ts export class ErrorTracker { private errors: Error[] = [] init() { // 全局错误捕获 window.addEventListener('error', (event) => { this.captureError(event.error || new Error(event.message)) }) // Promise拒绝捕获 window.addEventListener('unhandledrejection', (event) => { this.captureError(event.reason) }) // Vue错误处理 if (typeof app !== 'undefined' && app.config) { app.config.errorHandler = (err, instance, info) => { this.captureError(err, { componentName: instance?.$options?.name, lifecycle: info }) } } } captureError(error: Error, context?: any) { const errorInfo = { message: error.message, stack: error.stack, timestamp: Date.now(), url: window.location.href, userAgent: navigator.userAgent, context } this.errors.push(error as any) // 发送到错误追踪服务 this.sendToService(errorInfo) } private sendToService(errorInfo: any) { // 发送到Sentry、自建服务等 fetch('/api/errors', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(errorInfo) }).catch(console.error) } } // ========== 用户行为追踪 ========== // utils/analytics.ts export class UserBehaviorTracker { private events: any[] = [] trackPageView() { this.trackEvent('page_view', { url: window.location.href, referrer: document.referrer, title: document.title }) } trackClick(element: HTMLElement) { this.trackEvent('click', { tagName: element.tagName, id: element.id, className: element.className, text: element.textContent?.substring(0, 50) }) } trackScroll(depth: number) { this.trackEvent('scroll', { scrollDepth: depth, pageHeight: document.body.scrollHeight }) } trackEngagement(duration: number) { this.trackEvent('engagement', { duration, interactions: this.events.length }) } private trackEvent(type: string, data: any) { const event = { type, data, timestamp: Date.now() } this.events.push(event) this.sendEvent(event) } private sendEvent(event: any) { // 批量发送或实时发送 if (this.events.length >= 10) { this.flush() } } flush() { if (this.events.length === 0) return fetch('/api/analytics/events', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ events: this.events }) }).then(() => { this.events = [] }).catch(console.error) } } 7.2 调试工具 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 // ========== Vue DevTools集成 ========== // 开发环境启用调试功能 if (import.meta.env.DEV) { app.config.devtools = true // 添加组件名称便于调试 app.component('UserProfile', UserProfile) } // ========== React DevTools ========== // 使用React Developer Tools Profiler import { Profiler } from 'react' function onRenderCallback( id, phase, actualDuration, baseDuration, startTime, commitTime ) { console.log({ id, phase, actualDuration, baseDuration, startTime, commitTime }) } <Profiler id="UserProfile" onRender={onRenderCallback}> <UserProfile /> </Profiler> // ========== 自定义调试工具 ========== // utils/debug.ts export const debug = { log: (...args: any[]) => { if (import.meta.env.DEV) { console.log('[DEBUG]', ...args) } }, group: (label: string, fn: () => void) => { if (import.meta.env.DEV) { console.group(label) fn() console.groupEnd() } }, time: (label: string) => { if (import.meta.env.DEV) { console.time(label) } }, timeEnd: (label: string) => { if (import.meta.env.DEV) { console.timeEnd(label) } }, track: (name: string, value: any) => { if (import.meta.env.DEV) { console.log(`[TRACK] ${name}:`, value) } } } // 使用示例 debug.time('dataFetch') fetchData().then(data => { debug.track('fetchedData', data) debug.timeEnd('dataFetch') }) 总结 构建现代前端架构需要综合考虑多个维度: ...
深入探讨WebAssembly的性能优化技巧,从内存管理到SIMD优化,帮助开发者构建高性能的Web应用。
深入探讨用户体验设计的核心原则、心理学基础和实战技巧,帮助你设计出直观、高效、愉悦的数字产品。
深入探讨前端性能优化的核心技术,包括资源加载优化、渲染性能优化、网络优化和JavaScript性能优化,帮助你打造秒开的Web应用。
深入探讨响应式设计的核心原理、布局策略、媒体查询技巧和移动端适配方案,帮助你构建在各种设备上都完美展示的网站。
深入探讨Web可访问性(a11y)的核心原则、技术实现和测试方法,帮助你构建对所有人都友好的、符合WCAG标准的无障碍网站。
深入探讨现代API调试的高级技术和最佳实践,包括自动化测试、性能监控、错误追踪、安全调试和调试工具的使用,帮助开发者快速定位和解决API问题。
深入探讨2025年前端性能优化的最新技术和最佳实践,包括Core Web Vitals优化、代码分割、懒加载、缓存策略和现代Web性能工具的使用。
深入探讨现代前端开发中的调试技巧,包括浏览器DevTools高级功能、Source Map配置、断点调试、性能分析和常见问题排查方法。