我为什么不再用 oh-my-zsh 了
前言
这次折腾命令行环境,不是因为我想换一套更潮的工具,而是因为 Ghostty 里开一个 zsh,已经开始拖慢我每天的工作节奏了。
那种慢不是 benchmark 里才看得出来的慢,而是你每天开新 tab、开新窗口、切 shell 的时候都能感受到它在拖。对于一个长期把终端当主工作界面的人来说,这种拖沓会不断打断节奏。
一开始我以为问题出在终端本身,后面认真排查才发现,Ghostty 其实没什么问题,真正越来越重的是我那套已经用了很多年的 oh-my-zsh 环境。更准确地说,不是 oh-my-zsh 不能用,而是我的环境已经不适合继续让它来兜底了。
这篇文章主要想讲清楚三件事:我为什么不再继续用 oh-my-zsh,我最后留下来的命令行工具组合是什么,以及我是怎么把 Ruby / Node / Zsh 平稳迁过去的。
先说结论
如果只看结论,我这次迁移最核心的判断其实就一句话:
oh-my-zsh很适合快速搭环境,但不适合在一台主力开发机上无限叠加很多年。
我最后保留下来的组合是:
sheldonstarshipatuinzoxidemise
迁移前,我的交互式 zsh 启动时间大概在 2.43s;迁移后大概是 1.79s。这个提升当然有价值,但它不是这次迁移里最重要的部分。更重要的是,我终于把“谁在管插件、谁在管 prompt、谁在管 Ruby、谁在管 Node”这几个问题拆清楚了。
我现在回头看,这次迁移真正有价值的不是“换了一批新工具”,而是把一套越来越像历史包袱的命令行环境,重新整理成了一套边界清晰、能解释、能回滚的系统。
我为什么决定不再继续用 oh-my-zsh
先说清楚,我不是在批判 oh-my-zsh。
相反,我一直觉得它是一个非常好的起点。安装简单、开箱即用、插件多、默认 alias 也很顺手。对于刚开始认真折腾终端环境的人来说,它确实是性价比很高的选择。
但问题在于,oh-my-zsh 解决得很好的是“快速搭起来”,解决得不够好的是“长期极简维护”。
我之前那台机器上的环境很典型:
- 终端是
Ghostty - Shell 是
zsh - 插件框架是
oh-my-zsh - Ruby 走
rbenv - Node 一部分像
nvm,一部分又直接走 Homebrew - 另外还有语法高亮、自动建议、目录跳转、历史搜索、各种 alias 和第三方脚本初始化
这些东西单独看都合理,很多程序员机器上大概也差不多。真正的问题是,这些能力不是一次设计出来的,而是几年里一点一点叠上去的。最开始可能只是想加几个 git alias,后面慢慢又加了语法高亮、自动提示、版本管理、PATH 注入、shell integration、各种命令增强。每一项都不大,但它们都发生在 shell 启动阶段,最后就会把终端启动链路拖成一团。
我这次排查时,看到的问题都很典型:
zsh-syntax-highlighting被重复加载rbenv插件和手写rbenv init重复初始化brew shellenv在每个交互式 shell 里执行thefuck在初始化阶段就运行,而且还报权限错误
这些问题的共同点是,它们未必会把环境搞挂,但会持续制造隐形成本。更麻烦的是,当插件管理、prompt、历史、目录跳转、运行时管理、个人 alias 全都揉进同一个入口之后,你很难再回答几个很基本的问题:
- 谁在管插件?
- 谁在管 prompt?
- 谁在管 Ruby?
- 谁在管 Node?
- 谁在真正拖慢启动?
我最后放弃 oh-my-zsh,不是因为它“不好”,而是因为我的命令行环境已经复杂到不该继续靠一个大一统框架兜底了。
这次迁移,我真正想迁走的是什么
很多人看这类文章,容易把重点理解成“把 oh-my-zsh 换成另一个更快的框架”。但我这次真正想做的,其实不是单点替换,而是拆层。
我最后留下来的工具组合是:
sheldon负责插件管理starship负责 promptatuin负责历史搜索zoxide负责目录跳转mise负责 Ruby / Node 这类运行时管理
这套组合本身当然不是什么“终极方案”,换成别的工具也可以。但我这次最在意的不是工具名,而是边界。
以前我的命令行环境更像一个越长越大的入口文件,很多东西都堆在 .zshrc 里;现在我希望它更像几个职责明确的小模块。插件怎么加载,prompt 怎么渲染,命令历史怎么查,目录怎么跳,运行时谁说了算,各自有各自的归属。以后再出问题时,我至少知道应该去哪一层查,而不是一头扎进一个越来越长的 .zshrc 里靠猜。
所以如果一定要说这次迁移的核心收益,我会说不是“从 oh-my-zsh 换成了 sheldon”,而是“我终于把命令行环境从工具堆砌,整理成了结构”。
迁移过程里,真正的难点不在 zsh
这次迁移我没有采用“删掉重装”的方式,而是用了一个很保守的原则:
先拆结构,再并行接管,最后逐步切流。
这个原则对主力开发机很重要。因为你不是在玩具环境里做实验,而是在一台每天都要工作的机器上动基础设施。
我第一步做的不是改配置,而是先量化问题。交互式 zsh 启动时间当时大概在 2.43s。这一步很重要,因为你得先知道自己到底在优化什么,否则后面很容易陷入“感觉好像快了”的自我安慰。
接着我做的是把 shell 启动结构拆开。插件交给 sheldon,prompt 交给 starship,历史交给 atuin,目录跳转继续交给 zoxide,而我自己的 alias / function 继续留在 ~/.zshrc。这个阶段最明显的收益不是性能,而是认知负担下降了。以后再出问题,我知道该去哪层看。
真正棘手的是运行时管理,也就是 mise 接管 Ruby / Node 这一步。
Node 这部分相对顺利。我当时机器上实际在用的是 v24.10.0,所以我最关心的不是“命令有没有执行成功”,而是下面几件事:
1 | which node |
很多人迁移环境时只看版本号,但版本号一致不代表所有权真的切过去了。你看到的 node -v 可能没变,但背后到底是 Homebrew、nvm 还是 mise 在生效,完全可能不是一回事。对主力开发机来说,我更相信 which,而不是只相信版本号。
Ruby 才是这次迁移里真正的坑。我当时的目标版本是 2.7.8,一开始想直接让 mise 重新安装,结果下载阶段就失败了,核心报错大概是:
1 | curl: (56) Recv failure: Connection reset by peer |
这个问题很能说明真实迁移和教程文章之间的差别。教程里通常是“执行命令,安装成功,下一步”,但你在真实机器上迁环境,真正遇到的可能是网络抖动、老版本运行时、上游分发变化、镜像问题,或者别的任何外部因素。
我最后没有继续死磕“必须重新下载并编译一个全新的 Ruby 2.7.8”,而是换了一个更务实的思路:既然当前机器上已经有一份稳定工作的 Ruby 2.7.8,那就先让 mise 平稳接管它。对主力开发机来说,可用性通常比理论上的纯净更重要。
等 mise 确认已经能正确接管 ruby 和 node 之后,我才去改 ~/.zprofile,只清理那些明确已经过期的 PATH 残留,比如旧的 rbenv shim、旧的 nvm 版本路径,而不是一口气重写整条 PATH。PATH 这种地方一旦大改,很容易顺手把别的工具链也搞坏。
最后我做的不是“删旧环境”,而是“先准备回滚”。这次我不只是留了备份,还做了一键恢复脚本。因为开发环境迁移最怕的不是切换失败,而是切换失败之后你想退回去,却得靠记忆去想“我刚才到底改了哪几个文件”。
迁移做完之后,我又补了一轮“收口”
这次迁移还有一个很真实的后续动作:第一轮切换完成之后,我并没有马上把它当成“结束”。
原因很简单。which node 和 which ruby 虽然已经指向了 mise,但我后来复查交互式 shell 的 PATH,还是发现旧的 ~/.nvm/versions/...、~/.rbenv/shims、~/.rbenv/bin 继续留在最终环境里。这个问题最麻烦的地方在于,它不会立刻把环境搞挂,你甚至会以为迁移已经成功了,但它会在后面某次 PATH 顺序变化时,把所有权悄悄切回旧链路。
所以我后来新增了一条迁移原则:不要只验证“功能能不能跑”,还要验证“旧链路是不是已经真正退出”。对我来说,下面这些命令比只看 node -v、ruby -v 更有价值:
1 | which node |
我后来还补做了另外两类清理。一类是职责重叠的历史残留,比如目录跳转已经切到 zoxide,那旧的 autojump 就不该继续偷偷加载;另一类是只该在交互式 shell 里存在的能力,比如 bun、OpenClaw、fzf 这类补全和交互增强,不应该继续让脚本 shell 一起背启动成本。
这轮收口里,我还踩到一个很典型的坑:配置文件“看起来对”,不代表它真的能跑。比如后来补 Yazi 配置时,我就遇到过版本兼容问题,结果不是某个 opener 失效,而是工具本身直接起不来。那一刻我更确定了一件事:主力开发机上的环境迁移,最终不能靠肉眼判断配置长得像不像文档示例,而要靠真实启动验证。
1 | yazi --version |
最后我还把“私密信息”和“功能配置”拆开了。以前我习惯把 API Key、PATH 注入、代理 alias、函数和补全脚本全部堆进一个入口文件里。后来我越来越觉得,这种写法短期省事,长期却会持续放大认知负担。功能配置应该归功能配置,私密凭证至少应该单独放在私有 env 文件里,否则以后你很难分清自己是在排 shell 初始化问题,还是在排环境变量问题。
迁移之后,我最认可的不是速度,而是可解释性
从结果上看,这次迁移是值得的。交互式 zsh 启动时间大概从 2.43s 降到了 1.79s,体感上确实轻了一些。
但如果只用“快了 0.6 秒左右”来总结这次迁移,我觉得是低估了它。真正更有价值的是,很多以前说不清楚的问题,现在终于可以说清楚了。
比如:
- 插件是谁在管
- prompt 是谁在管
- 历史搜索是谁在管
- 目录跳转是谁在管
- Ruby / Node 是谁在管
这意味着以后再出问题时,我不用再对着一大坨历史配置靠经验猜,而是可以从结构上去排查。对于长期维护一台开发机的人来说,这种可解释性比单纯快一点更重要。
这次迁移里,我最想提醒的几件事
第一,不要一上来就删旧环境。尤其是 ~/.rbenv、~/.nvm、旧运行时目录这些东西,不要在切换当天就删。更稳的顺序应该是:新工具先进场,确认切换成功,正常使用几天,最后再清理旧目录。
第二,不要只看版本号,一定要看 which。ruby -v 和 node -v 只能告诉你“当前版本没变”,但它们不能告诉你这套环境到底归谁管。真正关键的是下面这些:
1 | which ruby |
第三,不要一次性重写整个 PATH。PATH 是最容易顺手把环境搞坏的地方之一。我的建议一直都是:尽量少动,只删明确已经过时的片段,不轻易重排其他工具链的顺序。
第四,主力开发机迁移一定要有回滚脚本。只做备份还不够,最好能直接一键恢复。因为当你真的需要回滚时,通常已经是在一个出问题的状态里了,这时候越依赖人工回忆,越容易二次踩坑。
第五,不要只看版本号和表面功能,一定要看 which、最终 PATH,以及旧链路有没有真的退出。
第六,completion、fzf、prompt 这类交互能力,尽量只在 interactive shell 里加载,不要把脚本 shell 也一起拖慢。
第七,不要迷信某一个工具。说到底,sheldon、starship、atuin、zoxide、mise 这些名字本身并不是重点。重点是边界是不是清楚,职责是不是独立,出问题时排查入口是不是明确。
写在最后
这次迁移做完之后,我最大的感受不是“终于换上了一套更潮的命令行工具”,而是:
命令行环境和代码一样,时间长了也会腐化。
很多程序员愿意花时间重构业务代码,但很少认真重构自己每天都在用的终端环境。可实际上,终端恰恰是我们日常开发里接触频率最高的工作界面之一。它启动慢、结构乱、问题难排查,这些成本不会一次性爆炸出来,但会一点一点吃掉注意力。
我现在并不觉得自己找到了什么终极方案,但至少这次我把它从一个越来越重的历史包袱,整理成了一套边界更清晰、维护更容易、也更容易解释的工具组合。对我来说,这已经足够值得。