Parin's Space
返回文章列表

Oh My Zsh → zsh + Starship 遷移筆記

· 閱讀時間 3 分鐘

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
promptOMZ themeStarship
pluginsOMZ 框架brew 原生套件

剩餘的 ~280ms 主要是 brew shellenv (~65ms) 和各種 CLI tool 的 shell integration init,是 login shell 的固定成本,跟 .zshrc 本身無關。.zshrc 內部實際只花 ~33ms。