Oh My Zsh → zsh + Starship 遷移筆記
zsh 啟動越來越慢,加上 Oh My Zsh 的功能自己其實只用到一小部分,趁這次整理直接換掉。最後啟動從 ~1000ms 降到 ~280ms。
先診斷問題在哪
不要盲猜,先加 profiling:
# ~/.zshrc 最上面
zmodload zsh/zprof
# ~/.zshrc 最下面
zprof
重開 terminal 就會輸出每個步驟的耗時,確認瓶頸再處理。我的情況是 nvm 佔了 90%:
nvm_auto 969ms 90.95% ← 元兇
nvm 347ms 32.63%
安裝套件
brew install zsh-autosuggestions zsh-syntax-highlighting zsh-completions starship
.zshrc 結構與幾個值得記的 pattern
Completion 快取(compinit 24h)
compinit 很慢,但其實不需要每次啟動都重跑,24 小時內用快取就好:
fpath=(
/opt/homebrew/share/zsh-completions
$fpath
)
autoload -Uz compinit
_zcompdump="${ZDOTDIR:-$HOME}/.zcompdump"
if [[ -n $_zcompdump(#qNmh-24) ]]; then
compinit -C -d "$_zcompdump"
else
compinit -d "$_zcompdump"
fi
unset _zcompdump
mh-24 表示「修改時間小於 24 小時」,走 -C 快速路徑跳過安全檢查。有個常見的寫法錯誤是把 glob 放在 [[ ]] 裡:
# ❌ 這樣寫 glob 不會展開,永遠走完整 compinit
[[ -n $path(#qN.mh+24) ]] && compinit -C || compinit
要先把 glob 展開結果存進變數再判斷(如上面的正確寫法)。修正後 compinit 從 ~900ms 降到 ~14ms。
NVM lazy load
NVM 是大戶,每次啟動都完整載入要幾百毫秒。改成 lazy load,第一次實際呼叫 node/npm 時才載入:
_nvm_load() {
unset -f nvm node npm npx yarn pnpm
[ -s "$(brew --prefix nvm)/nvm.sh" ] && . "$(brew --prefix nvm)/nvm.sh"
}
nvm() { _nvm_load; nvm "$@"; }
node() { _nvm_load; node "$@"; }
npm() { _nvm_load; npm "$@"; }
npx() { _nvm_load; npx "$@"; }
yarn() { _nvm_load; yarn "$@"; }
pnpm() { _nvm_load; pnpm "$@"; }
代價是第一次呼叫會有約 0.5 秒的一次性延遲,換取之後每次啟動都省掉這段時間。
kubectl completion 快取
kubectl completion zsh 每次執行都要幾十毫秒,存成檔案只跑一次:
_kubectl_completion_cache="$HOME/.zsh_kubectl_completion"
if command -v kubectl &>/dev/null; then
if [[ ! -f $_kubectl_completion_cache ]]; then
kubectl completion zsh > $_kubectl_completion_cache
fi
source $_kubectl_completion_cache
fi
unset _kubectl_completion_cache
kubectl 版本升級後需手動刪掉快取讓它重新產生:rm ~/.zsh_kubectl_completion
同樣的 pattern 可以套用在任何「產生 completion 很慢」的工具上。
History 設定
HISTFILE="$HOME/.zsh_history"
HISTSIZE=50000
SAVEHIST=50000
setopt HIST_IGNORE_DUPS HIST_IGNORE_SPACE SHARE_HISTORY
SHARE_HISTORY 讓多個 terminal window 之間共享歷史,避免在某個 window 打的指令在另一個找不到。
Options
setopt AUTO_CD # 直接打目錄名稱就 cd
setopt AUTO_PUSHD # 每次 cd 自動 pushd
setopt PUSHD_IGNORE_DUPS
Starship 設定
主要調整是讓 python/ruby/nodejs 只在有相關專案檔的目錄才顯示,不然 asdf shims 在 PATH 裡,每個目錄都會亮:
[gcloud]
disabled = true
[nodejs]
detect_files = ["package.json"]
detect_folders = []
detect_extensions = []
[python]
detect_files = ["requirements.txt", "pyproject.toml", "setup.py", ".python-version", "Pipfile"]
detect_folders = [".venv", "venv"]
detect_extensions = []
[ruby]
detect_files = ["Gemfile", ".ruby-version"]
detect_folders = []
detect_extensions = []
[username]
show_always = true
format = "[$user]($style)@"
[hostname]
ssh_only = false
format = "[$hostname]($style) "
[time]
disabled = false
format = "[$time]($style) "
time_format = "%H:%M:%S"
修復 compinit insecure directories
切換後可能看到這個 warning:
zsh compinit: insecure directories, run compaudit for list.
通常是 Homebrew 目錄的 group write 沒清掉:
chmod go-w '/opt/homebrew/share'
chmod -R go-w '/opt/homebrew/share/zsh'
結果
| 前 (OMZ) | 後 (zsh + Starship) | |
|---|---|---|
| 啟動時間 | ~1000ms | ~280ms |
| prompt | OMZ theme | Starship |
| plugins | OMZ 框架 | brew 原生套件 |
剩餘的 ~280ms 主要是 brew shellenv (~65ms) 和各種 CLI tool 的 shell integration init,是 login shell 的固定成本,跟 .zshrc 本身無關。.zshrc 內部實際只花 ~33ms。