Zsh の起動時間を短縮する
最近、Zsh を起動するのに時間がかかるのが気になっていたので、プロファイルを取って起動時間を短縮してみた。
ボトルネックを確認する
とりあえず Zsh の起動時間を確認する。体感的には 1.5 秒くらい。
% time zsh -i -c exit
zsh -i -c exit 0.61s user 0.93s system 113% cpu 1.349 total
どこがボトルネックになっているかを確認するには、zprof
というコマンドを使用する。以下の記事を参考にした。
# .zshrc の先頭行に追加
zmodload zsh/zprof
# ... 省略 ...
# .zshrc の最終行に追加
zprof
zprof
を追加して Zsh を起動すると、以下のような結果が表示される。
num calls time self name
-----------------------------------------------------------------------------------
1) 1 952.79 952.79 96.34% 605.40 605.40 61.21% nvm_auto
2) 2 347.39 173.69 35.12% 191.34 95.67 19.35% nvm
3) 1 136.50 136.50 13.80% 127.14 127.14 12.86% nvm_ensure_version_installed
4) 2 22.19 11.10 2.24% 22.19 11.10 2.24% compaudit
5) 1 19.42 19.42 1.96% 19.29 19.29 1.95% nvm_die_on_prefix
6) 1 35.42 35.42 3.58% 13.23 13.23 1.34% compinit
7) 1 9.35 9.35 0.95% 9.35 9.35 0.95% nvm_is_version_installed
8) 1 0.76 0.76 0.08% 0.76 0.76 0.08% colors
9) 1 0.14 0.14 0.01% 0.14 0.14 0.01% nvm_has
10) 4 0.13 0.03 0.01% 0.13 0.03 0.01% nvm_npmrc_bad_news_bears
11) 1 952.81 952.81 96.34% 0.02 0.02 0.00% nvm_process_parameters
12) 1 0.02 0.02 0.00% 0.02 0.02 0.00% nvm_is_zsh
Node.js のバージョンを管理するために使っている nvm がボトルネックになっているようだ。 とりあえず nvm に関する処理をコメントアウトしてみる。
-export NVM_DIR="$HOME/.nvm"
-[ -s "/usr/local/opt/nvm/nvm.sh" ] && . "/usr/local/opt/nvm/nvm.sh"
+#export NVM_DIR="$HOME/.nvm"
+#[ -s "/usr/local/opt/nvm/nvm.sh" ] && . "/usr/local/opt/nvm/nvm.sh"
体感で分かるほど起動が速くなった。
% time zsh -i -c exit
zsh -i -c exit 0.15s user 0.14s system 102% cpu 0.282 total
zprof
の結果からも nvm 関連の処理が消えている。
num calls time self name
-----------------------------------------------------------------------------------
1) 2 21.68 10.84 59.14% 21.68 10.84 59.14% compaudit
2) 1 35.90 35.90 97.95% 14.23 14.23 38.81% compinit
3) 1 0.75 0.75 2.05% 0.75 0.75 2.05% colors
nvm を遅延ロードする
とりあえず nvm をどうにかすれば良いことが分かったので、以下の記事を参考に必要な場合のみ nvm を遅延ロードするようにしてみた。
Zsh の Hook を使用し、カレントディレクトリを変更した場合に .nvmrc
が存在したら nvm をロードする。
function load-nvm () {
export NVM_DIR="$HOME/.nvm"
[[ -s $(brew --prefix nvm)/nvm.sh ]] && source $(brew --prefix nvm)/nvm.sh
}
load-nvmrc() {
if [[ -f .nvmrc && -r .nvmrc ]]; then
if ! type nvm >/dev/null; then
load-nvm
fi
nvm use
fi
}
autoload -Uz add-zsh-hook
add-zsh-hook chpwd load-nvmrc
Hook を追加した分、起動時間が増加するかと思ったが、この程度ではほぼ影響ないようだ。
% time zsh -i -c exit
zsh -i -c exit 0.15s user 0.15s system 102% cpu 0.289 total
このアイデアの問題は、cd
でディレクトリを移動する場合は Hook が動くが、Tmux でペインを分割するなどで Zsh を起動した場合は Hook が動かない。
nvm を使う場面はあまりないので、必要な時は load-nvm
を実行すれば良いかと思いそのままにしている。
なお、nvm がロードされていない場合、Homebrew でインストールしている Node.js が実行される。
dotfiles のコミットは以下。
evalcache を使って eval "$(foo init -)"
系の実行結果をキャッシュする
その他にも eval "$(foo init -)"
系の実行結果をキャッシュして起動時間を短縮するアプローチを見つけた。
evalcache
というツールを使用する。rbenv init -
といったコマンドはオーバーヘッドがあるし、結果は変わらないはずだからキャッシュすれば良いじゃん、ということらしい。
自分の環境ではプラグイン管理のツールを使っていないので、Git の Submodule として追加する。
git submodule add https://github.com/mroth/evalcache.git .zsh/evalcache
evalcache をロードし、eval "$(rbenv init -)"
などと書いている部分を _evalcache rbenv init -
に書き換える。
source $HOME/.zsh/evalcache/evalcache.plugin.zsh
if builtin command -v direnv > /dev/null; then
_evalcache direnv hook zsh
fi
if builtin command -v rbenv > /dev/null; then
_evalcache rbenv init -
fi
キャッシュを作成する前は少し時間がかかる。またメッセージが表示される。
% time zsh -i -c exit
direnv initialization not cached, caching output of: direnv hook zsh
rbenv initialization not cached, caching output of: rbenv init -
pyenv initialization not cached, caching output of: pyenv init -
zsh -i -c exit 0.15s user 0.16s system 102% cpu 0.303 total
キャッシュを作成した後は少し短縮されている。
% time zsh -i -c exit
zsh -i -c exit 0.10s user 0.10s system 103% cpu 0.196 total
今のところ不具合には遭っていないが、何か怪しい挙動があれば外すかも。
dotfiles のコミットは以下。
まとめ
Zsh にプロファイルを取る仕組みがあると分かったのが収穫だった。起動時間もだいぶ改善されて満足。
まだ compinit
や compaudit
など、補完機能に関する処理が残っているが、これ以上は体感的な速さも変わらないだろうということで、しばらくはこのままで使ってみようと思う。