暇人じゃない

Zsh の起動時間を短縮する
ZshNode.js

最近、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 にプロファイルを取る仕組みがあると分かったのが収穫だった。起動時間もだいぶ改善されて満足。

まだ compinitcompaudit など、補完機能に関する処理が残っているが、これ以上は体感的な速さも変わらないだろうということで、しばらくはこのままで使ってみようと思う。


About

chocoby (GitHub / Twitter)

個人事業主のソフトウェア開発者です。 Ruby と Rails を使った Web サービスの開発を得意としています。

CurryBu というサービスや、jp_prefecture という Gem を作っています。