前阵子在内网做技术分享,有个新同学问我:“一帧哥,现在 Vite、Turbopack 性能都这么猛了,前端工程化还有什么自己折腾的空间?”
讲真,十年前我刚开始写 Grunt、Gulp 脚本的时候,也是类似的想法,以为工具链无非就是配配打包器。但当你维护一个几百个组件的库,或者面对几十个仓库的 Monorepo 时,就会发现大工具只负责通用的“干道”,业务里到处都是泥泞的“小路”。
今天不聊虚的,就聊聊这两年我为了给团队提效,自己折腾前端工具链的一些具体实践和思考。
一、 别把所有问题都丢给构建工具
很多同学一提工程化,第一反应就是去写个 Webpack Loader 或者 Vite 插件。
不是说不行,但打包器本身已经够重了。举个实际例子:我们组件库里有将近 800 个 SVG 图标,每次发布前需要做三件事:
- 压缩 SVG 源码,剔除设计软件导出的垃圾属性。
- 提取图标的元数据(分类、关键词、拼音等),生成一个全量的
metadata.json供文档站搜索。 - 把每个 SVG 打包成独立的 React/Vue 组件,支持 Tree-shaking。
早期团队里有人在 Vite 插件里用 fs 同步去读这 800 个文件,本地起个 dev server 光是等图标插件跑完就要 6 秒多。
这就属于典型的“职责错配”。这种离线的数据处理,压根就不该塞进实时的开发服务器生命周期里。工具链的第一步,往往不是写插件,而是拆分阶段:把构建前的准备工作(Pre-build)独立成 CLI 工具,该走 CI 缓存走缓存,该离线生成走离线生成。
二、 从 Node.js 脚本到独立工具的瓶颈
一开始,这类活儿我们都是写个 .mjs 脚本,调一调 @swc/core 或者 Babel 完事。写起来顺手,前端同学都能改,但也有些恶心的问题:
- 环境依赖恶心:新同学拉下代码,Node 版本不对(比如老项目 Node 16,脚本用了 Node 18 的全局 fetch),直接报错。
node_modules幽灵:写脚本难免顺手装几个依赖,今天装个chalk,明天装个ora,一个简单的分析脚本,依赖树拉下来几十兆,CI 安装都要等半天。- 并发性能见顶:处理几千个文件的文本匹配和 AST 扫描时,Node.js 的单线程事件循环机制在 CPU 密集任务下跑得并不舒服,开
worker_threads写法又繁琐。
所以大概两年前,除了核心的 Web 业务代码,我开始把团队里一些底层的检测工具、代码生成器,改用 Golang 重构。
三、 为什么我喜欢用 Go 写前端工具
社区里现在都在吹 Rust,SWC、Rspack 确实快得离谱。但我个人偏好 Golang,理由很务实:学习成本低、开发效率高,而且 goroutine 搞并发太简单了。
前端工具大部分时候干的事情很简单:遍历文件 -> 正则/AST 解析 -> 提取特征 -> 输出文件。这是典型的 I/O + 轻度 CPU 计算。
放一段我用来扫描项目里废弃 CSS 类名的并发示例:
package main
import (
"fmt"
"os"
"path/filepath"
"sync"
)
// 并发收集所有 vue / tsx 文件的绝对路径
func collectFiles(root string) ([]string, error) {
var files []string
var mu sync.Mutex
var wg sync.WaitGroup
err := filepath.Walk(root, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
// 忽略目录和常见的无用文件夹
if info.IsDir() {
if info.Name() == "node_modules" || info.Name() == ".git" {
return filepath.SkipDir
}
return nil
}
ext := filepath.Ext(path)
if ext == ".vue" || ext == ".tsx" {
wg.Add(1)
go func(p string) {
defer wg.Done()
// 这里可以做一些轻量过滤,确认无误再加入
mu.Lock()
files = append(files, p)
mu.Unlock()
}(path)
}
return nil
})
wg.Wait()
return files, err
}
func main() {
files, err := collectFiles("./src")
if err != nil {
fmt.Printf("扫描失败: %v\n", err)
return
}
fmt.Printf("找到 %d 个目标文件\n", len(files))
}
改用 Go 之后,最大的爽点有两个:
- 单二进制分发:用 Go 交叉编译出
cli-darwin-arm64和cli-linux-amd64,直接丢进内网 CDN 或者通过 npm postinstall 下载二进制。开发者机子上哪怕没装 Node 都能跑,CI 里拉下来直接执行,0 秒依赖安装。 - 速度质变:以前跑一次 3 万个文件的 AST 引用依赖扫描,Node.js 脚本花了 42 秒;改用 Go 配合并发通道,只要 1.8 秒。
四、 自研 CLI 工具的几个避坑经验
折腾了几年工具链,踩过的坑比写的代码还多,总结了几条经验给想自研工具的同学:
1. 报错信息要说“人话”
很多前端工具最招人烦的就是崩了之后直接抛一屏堆栈跟踪:TypeError: Cannot read properties of undefined (reading 'split')。
没人关心你工具内部哪行代码挂了。使用者只想知道:是哪个文件的第几行配置写错了?
工具链开发必须做好防御性编程,把异常拦截并转换成友好的提示:
[Error] src/button/index.tsx 第 12 行: 找不到组件依赖 'icon-close',请确认是否已导出。
2. 克制你的配置项
刚开始写工具,很容易陷入“过度设计”的陷阱:总想做出第二个 Webpack,搞一堆 customOptions、hooks。
相信我,团队内部工具越死板越好。能约定大于配置(Convention over Configuration),就别给他们留参数。配置留得越多,后期答疑成本越高,升级的时候还要考虑一堆向下兼容。
3. 别迷信单一语言
如果你只是想写个小脚本校验一下 Git commit 格式,写个十行的小 JS 足够了,千万别为了用 Go 而用 Go,更别动不动就上 Rust。 选工具的核心逻辑就看两点:
- 频次与耗时:如果一天只跑一次,10 秒和 1 秒没区别;如果每次提交代码或合并 MR 都要跑,那耗时从 30 秒降到 2 秒就有巨大的收益。
- 环境隔离性:如果希望这个工具在脱离具体 Node 项目环境时也能作为全局基建使用,单二进制语言才是首选。
结语
前端做久了,往往会发现真正的效率瓶颈不在于框架用 Vue 还是 React,也不在于首屏渲染优化了几个毫秒,而是在于团队每天在重复、低效的链路里浪费了多少小时。
把日常繁琐的校验、生成、发布流程拆解开,针对卡点去写好用的工具,这个过程其实非常有成就感。不一定非要去参与那些几万 Star 的开源神作,解决掉手头具体项目里的那几秒卡顿,工程化的价值就已经落地了。
License: CC BY-NC 4.0
Updated 13 hours ago
Was this article helpful? Give it a like.
0 comments


