锦书在线
80.52M · 2026-03-21
想象一下这个场景:
我们正在写一个复杂的组件,思路如泉涌。保存文件,想看看效果:5 秒... 10 秒... 30 秒...
等页面刷新出来的时候,我们已经忘了刚才在想什么。心流被打断,灵感消失,只能重新理清思路。
这不是技术问题,这是对开发者时间的浪费。
根据 Stack Overflow 2023 年的调查,前端开发者平均每天要等待 30 - 60 分钟用于构建和热更新。
好消息是:这些等待时间,大部分都可以被优化掉。
本文将从最基础的概念讲起,用最通俗的语言,配合完整的代码示例,帮你一步步把开发环境的等待时间从“喝杯咖啡”缩短到“眨个眼”。
# 早上9点,开始工作
$ npm run dev
# 等待... 30 秒后项目终于启动了
# 打开浏览器,还要等 10 秒才能看到页面
# 修改一个文件,保存
# 等待... 10 秒后热更新完成
# 一天下来:
# 启动次数:10次 × 30 秒 = 300秒
# 修改次数:100次 × 10 秒 = 1500秒
# 总等待时间:1500秒 = 25分钟
这还只是保守估计。在大项目中,等待时间可能是这个数字的 3-5 倍。
开发环境的速度主要受四个因素影响:
node_modules.vue、.ts、.scss 等文件我们可以使用 Vite 的调试模式:
vite --debug
我们会看到类似这样的输出:
vite:deps 扫描依赖中... 245.3ms
vite:deps 找到 156 个依赖 245.3ms
vite:deps 预构建中... 3240.5ms ← 这里最慢!
vite:server 服务器启动完成 3512.8ms
根据输出结果,我们就可以做出正确的决断:
想象我们要整理一个巨大的图书馆(node_modules):
Vite 的预构建就是提前把第三方库整理成浏览器可以直接使用的格式。
Vite 默认会自动预构建,但它其实没有那么智能,以下场景,Vite 并不会预构件:
if (user.isAdmin) {
const Chart = await import('echarts') // 不会被预构建!
}
import { Button } from '@company/ui' // 不会被预构建!
import 'a' // a 依赖 b,b 依赖 c // c 可能不会被预构建!
// vite.config.js
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
export default defineConfig({
plugins: [vue()],
optimizeDeps: {
// 需要预构建的依赖
include: [
// 1. 体积大的库(减少请求数)
'echarts', // 原来可能有几百个文件,合并成一个
'lodash-es', // lodash-es 有 600+ 个文件!
'ant-design-vue', // UI 库通常都很大
// 2. Monorepo 中的本地包
'@company/ui',
'@company/utils',
'@company/hooks',
// 3. 动态导入的库
'monaco-editor', // 只在需要时加载,但预构建后加载更快
'xlsx', // 导出功能可能不常用,但需要时希望快
// 4. 有深层依赖的库
'date-fns', // 有很多子模块
'lodash' // 虽然不推荐,但如果用了就预构建
]
}
})
// vite.config.js
export default defineConfig({
optimizeDeps: {
exclude: [
// 1. 已经提供 ESM 格式的现代库
'vue', // Vue 本身已经优化好
'vue-router', // 不需要再打包
'pinia',
// 2. 很少用到的大库(按需加载更好)
'pdfjs-dist', // 只在查看 PDF 时用到
'three', // 只在 3D 页面用到
// 3. 有特殊构建要求的库
'@sentry/browser', // 有自己的构建工具
'firebase' // 复杂的构建配置
]
}
})
遇到一个依赖 →
↓
是本地包(@company/xxx)? → 是 → include
↓否
是动态导入的? → 是 → include
↓否
体积 > 1MB? → 是 → include(除非很少用)
↓否
依赖深度 > 3层? → 是 → include
↓否
已提供 ESM 格式? → 是 → 可以 exclude
↓否
用默认行为
// scripts/analyze-deps.js
import fs from 'fs'
import path from 'path'
// 分析 node_modules 中哪些包体积大
function findHeavyDeps() {
const nodeModules = path.resolve('node_modules')
const deps = fs.readdirSync(nodeModules)
.filter(d => !d.startsWith('.'))
.map(dep => {
const pkgPath = path.join(nodeModules, dep)
try {
const stats = fs.statSync(pkgPath)
return { name: dep, size: stats.size }
} catch {
return { name: dep, size: 0 }
}
})
.sort((a, b) => b.size - a.size)
.slice(0, 20) // 前20个最大的
console.log('体积最大的依赖:')
deps.forEach(d => {
console.log(`${d.name}: ${(d.size / 1024 / 1024).toFixed(2)}MB`)
})
}
findHeavyDeps()
Vite 默认会项目中的所有文件。在大型项目中,这可能会导致很多问题:
// vite.config.js
export default defineConfig({
server: {
watch: {
// 不要这些文件夹
ignored: [
'**/node_modules/**', // 依赖包,不需要
'**/dist/**', // 构建输出,不需要
'**/.git/**', // git 目录
'**/.idea/**', // IDE 配置
'**/.vscode/**', // VSCode 配置
'**/*.log', // 日志文件
'**/coverage/**', // 测试覆盖率报告
'**/tests/**', // 测试文件(通常不需要热更新)
'**/__tests__/**', // 同上
'**/__mocks__/**' // Mock 文件
],
// 只在需要的地方
// 默认会整个项目,但我们可以更精确
paths: [
'src/**', // 源代码
'index.html', // 入口文件
'vite.config.js' // 配置文件
]
}
}
})
修改文件
↓
Vite 发现变化
↓
重新编译这个文件
↓
找出所有依赖这个文件的模块(可能很多!)
↓
重新编译所有受影响的模块
↓
通过 WebSocket 通知浏览器
↓
浏览器请求新模块
↓
执行更新
// 不好的做法:一个文件导入太多东西
// UserManagement.vue
import { useUserStore } from '@/stores/user'
import { usePermissionStore } from '@/stores/permission'
import { useSettingsStore } from '@/stores/settings'
import UserList from './UserList.vue'
import UserForm from './UserForm.vue'
import UserFilters from './UserFilters.vue'
import UserStats from './UserStats.vue'
// ... 20 个 import
// 好的做法:按需加载,拆分组件
// UserManagement.vue
import { useUserStore } from '@/stores/user' // 只导入需要的
// 其他组件通过异步加载
const UserList = defineAsyncComponent(() => import('./UserList.vue'))
const UserForm = defineAsyncComponent(() => import('./UserForm.vue'))
const UserFilters = defineAsyncComponent(() => import('./UserFilters.vue'))
// 在组件中明确告诉 Vite 如何处理更新
if (import.meta.hot) {
// 1. 接受自身更新(默认行为)
import.meta.hot.accept()
// 2. 只接受某些依赖的更新
import.meta.hot.accept(['./api.js', './utils.js'], (modules) => {
console.log('API 或工具函数更新了')
// 重新执行某些逻辑
})
// 3. 拒绝更新(某些模块不适合热更新)
import.meta.hot.decline('./heavy-chart.js')
// 4. 清理资源(更新前执行)
import.meta.hot.dispose(() => {
// 清理定时器、事件器等
clearInterval(timer)
window.removeEventListener('resize', handler)
})
}
// vite.config.js
export default defineConfig({
css: {
// 开发时的 CSS 选项
devSourcemap: false, // 关闭 sourcemap,加快速度
preprocessorOptions: {
scss: {
// 缓存编译结果
implementation: 'sass',
// 避免使用 fiber(会导致热更新慢)
fiber: false,
// 全局注入变量(只注入需要的)
additionalData: `@import "@/styles/variables.scss";`
}
}
}
})
// vite.config.js
export default defineConfig({
// 使用 esbuild 替代 tsc 进行 TypeScript 转译
esbuild: {
target: 'es2020',
// 启用 esbuild 的 JSX 编译
jsxFactory: 'h',
jsxFragment: 'Fragment',
// 排除不需要转译的文件
include: /.(ts|jsx|tsx)$/,
exclude: /node_modules/
},
// 生产构建时才使用 TypeScript 检查
plugins: [
vue(),
// 开发环境不检查类型,加快速度
process.env.NODE_ENV === 'production' && tsChecker()
]
})
内存占用主要来自:
// vite.config.js
export default defineConfig({
server: {
// 模块缓存限制
moduleCache: {
maxSize: 500 // 最多缓存 500 个模块
},
// 模块图清理间隔
moduleGraph: {
pruneInterval: 60000 // 每 60 秒清理一次未使用的模块
}
},
// 开发环境关闭 sourcemap
build: {
sourcemap: false
},
// 限制处理的文件大小
esbuild: {
exclude: [/.(png|jpe?g|gif|webp|mp4|webm|ogg|mp3|wav|flac|aac)$/]
}
})
// 在 vite.config.js 中添加内存坚控
export default defineConfig({
plugins: [
{
name: 'memory-monitor',
configureServer(server) {
let timer = setInterval(() => {
const used = process.memoryUsage().heapUsed / 1024 / 1024 / 1024
if (used > 1.5) { // 超过 1.5GB
console.log(` 内存使用 ${used.toFixed(2)}GB,正在清理...`)
// 清理模块缓存
server.moduleGraph.clear()
// 强制垃圾回收(如果可用)
if (global.gc) {
global.gc()
}
}
}, 60000) // 每分钟检查一次
// 服务器关闭时清理定时器
server.httpServer?.on('close', () => {
clearInterval(timer)
})
}
}
]
})
// vite.config.js
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import { dependencies } from './package.json'
// 需要预构建的重型依赖
const heavyDeps = [
'echarts',
'ant-design-vue',
'lodash-es',
'xlsx',
'monaco-editor',
'd3',
'three',
'@company/ui',
'@company/utils',
'@company/charts'
]
// 不需要预构建的现代库
const esmDeps = ['vue', 'vue-router', 'pinia', 'vueuse']
export default defineConfig({
plugins: [vue()],
// 依赖优化
optimizeDeps: {
include: heavyDeps,
exclude: esmDeps,
// 使用 esbuild 加速
esbuildOptions: {
target: 'es2020',
define: {
'process.env.NODE_ENV': '"development"'
}
}
},
// 开发服务器配置
server: {
// 启用 HTTP/2 加速请求
https: true,
http2: true,
// 文件优化
watch: {
ignored: [
'**/node_modules/**',
'**/dist/**',
'**/.git/**',
'**/.idea/**',
'**/.vscode/**',
'**/*.log',
'**/coverage/**',
'**/tests/**',
'**/__tests__/**',
'**/__mocks__/**'
]
},
// 内存优化
moduleCache: {
maxSize: 500
},
// 热更新优化
hmr: {
timeout: 5000,
overlay: false // 关闭错误覆盖,加快速度
}
},
// 编译优化
esbuild: {
target: 'es2020',
include: /.(ts|jsx|tsx)$/,
exclude: /node_modules|.(png|jpe?g|gif|webp|mp4)$/,
jsxFactory: 'h',
jsxFragment: 'Fragment'
},
// CSS 优化
css: {
devSourcemap: false,
preprocessorOptions: {
scss: {
implementation: 'sass',
fiber: false,
additionalData: `@import "@/styles/variables.scss";`
}
}
}
})
{
"scripts": {
"dev": "vite",
"dev:debug": "vite --debug",
"dev:fresh": "rm -rf node_modules/.vite && vite",
"dev:profile": "vite --profile",
"build": "vite build",
"preview": "vite preview",
"analyze": "node scripts/analyze-deps.js"
}
}
| 可能原因 | 解决方案 |
|---|---|
| 预构建太多 | 优化 include 配置 |
| 文件范围太大 | 配置 watch.ignored |
| 依赖版本冲突 | 删除 node_modules 重装 |
| 磁盘 I/O 瓶颈 | 迁移到 SSD |
| 可能原因 | 解决方案 |
|---|---|
| 模块图过大 | 拆分大组件 |
| 没有定义热更新边界 | 使用 import.meta.hot.accept() |
| CSS 编译慢 | 优化预处理器配置 |
| 浏览器卡顿 | 关闭不必要的扩展 |
| 可能原因 | 解决方案 |
|---|---|
| 缓存太多 | 限制 moduleCache.maxSize |
| 没有垃圾回收 | 添加内存坚控和清理 |
| sourcemap 太大 | 关闭 devSourcemap |
| 内存泄漏 | 检查插件和代码 |
vite --debug 分析启动时间include 包含所有重型依赖exclude 排除了已优化的依赖记住:开发者的时间比机器的时间更宝贵。花一个小时优化开发环境,可能每天能为团队节省数小时的等待时间。这是性价比最高的投资之一。
对于文章中错误的地方或有任何疑问,欢迎在评论区留言讨论!