zshの起動が遅いのでなんとかしたい

関連記事

- [zshの起動が遅いのでなんとかしたい 2](https://qiita.com/vintersnow/items/c29086790222608b28cf)
- [最速のZsh プラグインマネージャーを求めて](https://qiita.com/vintersnow/items/cef72aebb2d55a82f212)

# 概要
一言 : zshの起動が遅いのでなんとかしたい

tmuxでペインをどんどん作っては消したりしているとzshに起動の遅さでイライラしてくる。これは精神的に良くないのでできるだけ速くしてみた。

# 前提

[dotfiles](https://github.com/vintersnow/dotfiles/blob/master/.zshrc)をあげているのでそれを見てくれると早いと思うがだいたい以下の様な流れで起動する。
環境はMac10.11、zsh5.2(brewでインストール)である。

+ zshrcから`.zsh.d/*.zsh`を読み込む
+ プラグインマネージャーの[zplug](https://github.com/zplug/zplug)を使ってプラグインをロード
    + 見た目は[sindresorhus/pure](https://github.com/sindresorhus/pure)。シンプルで速い。
+ tmuxが立ちあがってなければtmuxを起動

また、oh-my-zshやpreztoなどのフレームワークは使わない。理由は以下のとおり。

+ 遅いと思われる。（実際試してないからなんとも言えないが…）
+ それに設定を自分で一度書いてしまったので移行するコストがめんどくさい。

時間の測定は以下のように、インタラクティブシェルの起動速度で測っている（終了する処理も含まれてしまうが）。

```console
$ time ( zsh -i -c exit )
```

一回だけだとバラツキやすいので、以下のようにループするようにした。

```sh
for i in $(seq 1 10); do time zsh -i -c exit; done
```

# 高速化

## zshの限界
そもそもzshはどれぐらいで起動するのか測ってみた。

```console
$ : > /tmp/.zshrc
$ ( ZDOTDIR=/tmp/ zsh -i -c exit; )  0.00s user 0.01s system 65% cpu 0.012 total
```

空のzshrcで0.012秒。これがzshの限界のようだ。

## 現状
さて、チューニング前の起動速度を測ってみた。

```
( zsh -i -c exit; )  0.53s user 0.54s system 84% cpu 1.255 total
```

1.2秒かかっている。powerlineとか使ってたころはもっと遅かったと思うが、今ではこれでも遅いと感じてしまう。慣れとは恐ろしい。

## プロファイル
とりあえず、なにがボトルネックとなっているのかzprofでプロファイリングしてみる。

`.zshenv`の最初と`.ｚshrc`の最後に以下を付け足す。

```:.zshenv
zmodload zsh/zprof && zprof
```

```sh:.zshrc
if (which zprof > /dev/null 2>&1) ;then
  zprof
fi
```

これでzshを再起動したらプロファイリングが出てくる。プロファイリングしないときは、`.zshenv`の1行をコメントアウトすればいい。

結果は以下のとおり

```console
$ exec $SHELL -l
...
 1)    3         754.93   251.64   77.10%    417.11   139.04   42.60%  compinit
 2)    1         164.71   164.71   16.82%    164.71   164.71   16.82%  compdump
 3)    4         125.24    31.31   12.79%    124.28    31.07   12.69%  __parser__
 4)  751         119.53     0.16   12.21%    119.53     0.16   12.21%  compdef
 5)    4          53.78    13.44    5.49%     53.78    13.44    5.49%  compaudit
 6)    1         103.18   103.18   10.54%     34.33    34.33    3.51%  __zplug::zplug::cache::load
 7)    1          27.25    27.25    2.78%     27.03    27.03    2.76%  _zsh_highlight_load_highlighters
 8)    6         253.20    42.20   25.86%      8.66     1.44    0.88%  zplug
```

見方が自信ないけど

```
 1)    3(呼び出し回数)         754.93（合計実行時間）   251.64(1回あたりの実行時間)   77.10%    417.11（自分自身の合計実行時間）   139.04（自分自身の一回あたりの実行時間）    42.60%  compinit（関数名）
```
なのかな？
自分自身の実行時間というのは`source`などで外部ファイルを読み込んだ時にかかる時間を除いた時間だろう。

ともあれ、`compinit`と`zplug`が時間かかっている。`compdef`もかかっているように見えるが、今時zcompdumpを削除したため必要となっただけで、普段からする処理ではないので無視する。

これを元に、`zsh.d`以下のファイルをコメントアウトしていって、どれがボトルネックになっているのか探ってみた。
結果以下のファイルを読み込まないようにしたら62msで起動した。

+ 1develop.zsh : anyenvなどを読み込んでいる。
+ zplug_manager.zsh　: プラグインマネージャーのzplugの設定、ロードをおこなっている。


## compinit
compinitはzshの補完をするために必要なものなんで、おおよそ全てのzshrcに書かれているのではないかと思う。
しかし基本的に1回でいいので、3回も呼び出される必要はない。
最初はzplugでロードしたプラグインの中で呼び出されてるのかと思ったが[zplugはなんとcompinitを一回だけ呼び出すようになっているらしい](https://github.com/zplug/zplug/issues/24)！すばらしい。

原因は`1develop.zsh`で読み込んでいたBluemixのzsh_autocompleteの中でcompinitが呼ばれていたからだった。
該当部分をコメントアウトし、またzplugでcompinitが呼ばれるなら、自分で呼ぶ必要もないため`.zshrc`からもcompinitを消した。
これでcompinitが1回になった。

## zplug
次にzplug。
zplugはプラグインマネージャーの中では早いが、200msぐらいはかかっているようだ。
結局プラグインは4つしかロードしていないのでマネージャーを使わない方が早いのかもしれないが、流石にそこまでするのはめんどくさい。
仕方ないので、未インストールのチェックを飛ばすことにした。

```sh
if ! zplug check --verbose; then
  printf "Install? [y/N]: "
  if read -q; then
    echo; zplug install
  fi
fi
```

上記の処理がzplugのサンプルでは書いてあると思うが、これをコメントアウトした。プラグインを追加した時、手動でインストールしないといけなくなるが、そう頻繁にあることではないし、まあいいかと思う。これで0.1s高速化した。

zgenも試してみたが、そんなに変わらなかった（170~220ms）。しかし、自動インストールも含めての速度なのでzgenの方が自動インストールを含めると早いということになる。zgenのチェックは6msぐらいしかかかっていないかったのでおそらくzplugとは違う方法で行っているのだろう。

まだ調べていないが、zplug特有の高速化もあるかもしれないので調べてみたい。

## anyenv
`1deveolp.zsh`の中でも`eval "$(anyenv init - )"`が処理の大半を占めていた。(zprofには出てこないのだが…)

```sh
❯ time (eval "$(anyenv init - )")
( eval "$(anyenv init - )"; )  0.14s user 0.19s system 95% cpu 0.347 total
```

これは遅い！
とりあえず、`--no-rehash`をつけてみる。

```sh
❯ time ( eval "$(anyenv init - --no-rehash)" )
( eval "$(anyenv init - --no-rehash)"; )  0.09s user 0.11s system 84% cpu 0.229 total
```

[initで書き出しされるスクリプトを保存しておき、それを読み込むことで高速化できるよう](http://blog.uu59.org/2014-01-06-fast-rbenv.html)だが、anyenvだとめんどくさそう。
とりあえず`anyenv init`自体`anyenv-init`のラッパーなので実態を直接叩くことで少し高速化すると思う。

```sh
❯ time (eval "$(env PATH="$ANYENV_ROOT/libexec:$PATH" $ANYENV_ROOT/libexec/anyenv-init - --no-rehash)")
( eval ; )  0.07s user 0.10s system 87% cpu 0.192 total
```

# zcompile
今まで知らなかったのだが、`zcompile`で`.zshrc`をコンパイルすることが出来る。これで外部ファイルの読み込みが速くなるはず。
変更した時自動でコンパイルされるように以下をzshrcに加えた。

```sh
if [ $DOTFILES/.zshrc -nt ~/.zshrc.zwc ]; then
  zcompile ~/.zshrc
fi
```

ただ、自分の環境だとあんまり変化はなかった。せいぜい0.03sほど速くなった気がする。


# 結果

```sh
❯ time ( zsh -i -c exit )
Static loading...
( zsh -i -c exit; )  0.17s user 0.17s system 92% cpu 0.368 total
```

3倍ほど高速化した！
けどまだ少しラグを感じる。0.3sを切るとだいぶ早く感じるのであと0.1s頑張りたい。

#追記(8/2)

一晩経ったら、0.5sまで遅くなっていた。おそらくcacheかなにかが効いていたのだろう。

## anyenv 遅延ロード
やはりanyenvが遅いので少し手を加えた。
そもそもrbenvとかって毎回ロードする必要があるのだろうか？おそらく無いだろう。rbenvで設定したrubyが使えたら大抵の場合問題はない。
なので、rbenvなどは遅延ロードすることにした。

```bash
# anyenv
export ANYENV_ROOT="$(ghq root)/github.com/riywo/anyenv"
if [ -d $ANYENV_ROOT ]; then
  export PATH="$ANYENV_ROOT/bin:$PATH"
  for D in `command ls $ANYENV_ROOT/envs`
  do
    export PATH="$ANYENV_ROOT/envs/$D/shims:$PATH"
  done
fi

function anyenv_init() {
  eval "$(anyenv init - --no-rehash)"
}
function anyenv_unset() {
  unset -f ndenv
  unset -f rbenv
}
function ndenv() {
  anyenv_unset
  anyenv_init
  ndenv "$@"
}
function rbenv() {
  anyenv_unset
  anyenv_init
  rbenv "$@"
}
```

欠点は*envごとに関数を用意しないといけないこと。もはやanyenvの利点とは…
いっそことanyenvに組み込んだらいいのでは？という気がする。

追記:
上記の遅延ロードを行ったところ、`npm install -g`でグローバルにインストールしたパッケージがbinにエイリアスされなくなった。`eval "$(ndenv init)"`すれば治る。

## 結果

```sh
❯ time ( zsh -i -c exit )
( zsh -i -c exit; )  0.16s user 0.12s system 99% cpu 0.273 total
❯ time (zplug load)
( zplug load; )  0.10s user 0.07s system 93% cpu 0.174 total
```

これでイライラは大分解消された！
ただ、たまにzplugのloadが遅い時があるのだが（0.6sほど）、zplugのcacheを使う条件はどうなっているのか調べてみたい。

## 16/10/18追記

手動でプロファイルしていると、PATHを設定している`env_path.zsh`や`develop.zsh`、aliasを設定している`alias.zsh`が不思議と時間かかっている。  
そんなはずはないので原因を探してみたところ、パス解決に`brew --prefix`や`ghq root`を使っていたからのようだ。
これが一回あたり0.02~0.03sかかっているみたい。なのでこれを固定値に変えた。

今のところこんな感じ。

```sh
❯ for i in $(seq 1 10); do time zsh -i -c exit; done
zsh -i -c exit  0.10s user 0.08s system 99% cpu 0.178 total
zsh -i -c exit  0.08s user 0.07s system 101% cpu 0.151 total
zsh -i -c exit  0.09s user 0.07s system 102% cpu 0.157 total
zsh -i -c exit  0.10s user 0.08s system 86% cpu 0.210 total
zsh -i -c exit  0.09s user 0.07s system 96% cpu 0.169 total
zsh -i -c exit  0.09s user 0.07s system 101% cpu 0.154 total
zsh -i -c exit  0.09s user 0.07s system 102% cpu 0.150 total
zsh -i -c exit  0.10s user 0.08s system 103% cpu 0.169 total
zsh -i -c exit  0.09s user 0.07s system 102% cpu 0.155 total
zsh -i -c exit  0.09s user 0.07s system 101% cpu 0.156 total
```

zplugが0.1~sかかっているので、これ以上求めるならプラグインを諦めないといけなさそう。

## 16/12/13追記
[zplug諦めた](http://qiita.com/vintersnow/items/c29086790222608b28cf)