环境:macOS arm64 (Apple Silicon),32GB RAM,Ghostty 终端

这台机器用了好几年了。各种开发环境装了又卸、卸了又装,.zshrc.bash_profile 里塞满了历史遗留的 PATH、重复的插件、已经不存在的目录。终端启动越来越慢,每次开个新 tab 都要愣一下。

一直知道该清理,但总觉得"下次再说"。直到最近用 Claude + GLM 5.1 试了一下,从诊断到改完,前后大概十来分钟。

结果是从 1.3 秒降到了 0.2 秒。

先看看有多慢

time zsh -i -c exit

跑一下,1.303 秒。开个终端等一秒多,体感确实很差。

怎么查的

整体计时知道了慢,但不知道慢在哪。有两种方式定位:

逐个 source 测耗时:

time zsh -c "source /opt/homebrew/opt/nvm/nvm.sh"
time zsh -c "source $HOME/.sdkman/bin/sdkman-init.sh"
time zsh -c "eval \$(/opt/homebrew/bin/brew shellenv)"

zprof 函数级分析:

.zshrc 开头加 zmodload zsh/zprof,末尾加 zprof,能得到精确到每个函数的耗时排名。这个最管用。

到底是什么在拖后腿

zprof 一跑,问题很清楚:

  • nvm 吃了 ~540ms,占了一半多。它每次启动都扫描所有 Node 版本,完全没必要。
  • compinit(补全系统)~360ms,6 个插件的补全初始化开销。
  • SDKMAN ~300ms,Java 版本管理器初始化。
  • brew / bun / cargo 这些加起来 ~120ms,影响不大。

但除了这些"大头",还有一堆乱七八糟的问题:

  • zsh-syntax-highlightingfast-syntax-highlighting 功能完全一样,同时加载着
  • ServBay 环境块在 .zshrc.bash_profile 各出现一次
  • SDKMAN 也加载了两次,同上
  • Antigravity PATH 在 .zshrc 里写了两次
  • datax、codebuddy、yarn global 等目录早就不存在了,PATH 还留着
  • .zshenv 里重复加载 cargo/env
  • arch -arm64 brew 这个 alias 在原生 arm64 机器上完全多余

说白了,就是年久失修。每次装个新工具就往配置文件里塞一行,从来没清理过。

改了什么

nvm 懒加载(省 ~500ms,效果最大)

原来每次开终端都加载 nvm,但大部分时候根本不用 node。改成懒加载——首次调用 node/npm/npx 时才初始化:

export NVM_DIR="$HOME/.nvm"
lazy_load_nvm() {
  unset -f nvm node npm npx pnpm
  [ -s "/opt/homebrew/opt/nvm/nvm.sh" ] && \. "/opt/homebrew/opt/nvm/nvm.sh"
  [ -s "/opt/homebrew/opt/nvm/etc/bash_completion.d/nvm" ] && \. "/opt/homebrew/opt/nvm/etc/bash_completion.d/nvm"
}
nvm()  { lazy_load_nvm; nvm "$@"; }
node() { lazy_load_nvm; node "$@"; }
npm()  { lazy_load_nvm; npm "$@"; }
npx()  { lazy_load_nvm; npx "$@"; }
pnpm() { lazy_load_nvm; pnpm "$@"; }

SDKMAN 懒加载(省 ~300ms)

同理,java/sdk/mvn 这些也不需要每次都加载:

export SDKMAN_DIR="$HOME/.sdkman"
lazy_load_sdkman() {
  unset -f sdk java javac jar mvn gradle
  [[ -s "$HOME/.sdkman/bin/sdkman-init.sh" ]] && source "$HOME/.sdkman/bin/sdkman-init.sh"
}
sdk()    { lazy_load_sdkman; sdk "$@"; }
java()   { lazy_load_sdkman; java "$@"; }
javac()  { lazy_load_sdkman; javac "$@"; }
jar()    { lazy_load_sdkman; jar "$@"; }
mvn()    { lazy_load_sdkman; mvn "$@"; }
gradle() { lazy_load_sdkman; gradle "$@"; }

精简插件

去掉了 zsh-autocomplete(bug 多、资源占用高)和 fast-syntax-highlighting(跟另一个重复):

# 之前
plugins=(git poetry zsh-autosuggestions zsh-syntax-highlighting fast-syntax-highlighting zsh-autocomplete)

# 之后
plugins=(git poetry zsh-autosuggestions zsh-syntax-highlighting)

清理重复和失效的配置

删掉的东西不少,列一下:

重复的:

  • ServBay 环境块(.zshrc 里有,.bash_profile 里也有)
  • SDKMAN 加载(同上)
  • Antigravity PATH(.zshrc 自己内部就写了两次)
  • cargo/env(.zshenv.bash_profile 都加载了)

失效的:

  • $PYENV_ROOT/bin — 目录不存在
  • DATAX_HOME — 目录不存在
  • $PATH. 末尾多了个 . — 语法错误
  • yarn/global/node_modules/.bin — 目录不存在
  • .codebuddy/bin — 目录不存在
  • arch -arm64 brew alias — arm64 原生机器不需要这个包装

最终结果

阶段启动时间
优化前1.303s
懒加载 + 插件精简后0.549s
清理失效 PATH 后0.208s

从 1.3 秒到 0.2 秒,快了大概 6 倍。

有两个小事项:每个终端会话里首次跑 nodejava 会有一次 0.5-0.8 秒的加载延迟,之后就不会了。另外 zsh -i -c exit 测试时可能会报 gitstatus 初始化失败,这是因为非交互模式缺少完整 TTY,正常使用不会碰到。

整个过程,从打开终端发现问题到改完验证,十几分钟。之前拖了大半年没动的东西,让 AI 帮忙梳理一下就清掉了。这种"知道自己该做但一直没做"的事情,有个帮手确实不一样。