爆速で起動する Neovim を作る

# 1. ayamir/nvimdots の衝撃

このエントリーを書くきっかけとなったのは以下のレポジトリーを見たことでした。

* [ayamir/nvimdots: A well configured and structured Neovim.][ayamir/nvimdots]

[ayamir/nvimdots]: https://github.com/ayamir/nvimdots

これは Neovim の設定ファイルを公開しているレポジトリーです。特徴的なのはその起動速度。Neovim 界の三種の神器、[nvim-telescope/telescope.nvim][]、[nvim-lualine/lualine.nvim][]、[hrsh7th/nvim-cmp][] を含む、割合新し目で重たいプラグインを使いながら、**10 ミリ秒**台の起動速度を実現しているのです。

[nvim-telescope/telescope.nvim]: https://github.com/nvim-telescope/telescope.nvim
[nvim-lualine/lualine.nvim]: https://github.com/nvim-lualine/lualine.nvim
[hrsh7th/nvim-cmp]: https://github.com/hrsh7th/nvim-cmp

これに触発され、僕自身の設定も大幅に見直すことができ、ayamir/nvimdots とほとんど遜色無い速度を実現することができました。この記事ではその設定に使った様々なテクニックを紹介します。

なお、僕の設定例は以下のレポジトリーの `.config/nvim` ディレクトリに纏めています。

* [delphinus/dotfiles: my dotfiles](https://github.com/delphinus/dotfiles)

:::note warn

1. 利用している Neovim は記事時点の `HEAD`、つまり開発中の最新版です。利用するバージョンによっては使えない機能があるかも知れません。
2. プラグインマネージャーには [wbthomason/packer.nvim][packer.nvim] を利用しています。読者としてはこのプラグインについて基本的な使い方を理解している人が対象です。以前解説記事も[書いております][前回の記事]ので、適宜参照してください。

[packer.nvim]: https://github.com/wbthomason/packer.nvim

:::

:::note info

ayamir/nvimdots を自分の環境で試す時はちょっと注意が必要です。これはこの記事の本題とズレてしまうので、一番最後に「9. おまけ」として記載しています。

:::

# 2. 起動速度の測り方

**10 ms** というのがどのくらい凄いことなのか、そもそも起動速度を測ったこともない人も多いと思うのでそこから説明していきます。

## 2.1. `--startuptime` オプション

CLI でサッと起動できてすぐに編集が始められるのが (Neo)Vim のいいところです。それは存在意義と言ってもいいでしょう。そのため (Neo)Vim 本体にも起動速度を細かく調べる機能が付いています。

```sh
$ nvim --startuptime ~/nvim.log +q
```

:::note info

`--startuptime` は Neovim だけでなく Vim でも使える機能です。

:::

このコマンドでは `~/nvim.log` に以下のようなログを記録した上ですぐに終了しています。

```
times in msec
 clock   self+sourced   self:  sourced script
 clock   elapsed:              other lines

000.003  000.003: --- NVIM STARTING ---
000.076  000.073: event init
000.151  000.075: early init
000.646  000.494: locale set

……（省略）……

031.783  000.604  000.604: require('vim.diagnostic')
031.785  000.724  000.120: require('scrollbar.handlers.diagnostic')
031.922  000.077  000.077: require('scrollbar.handlers')
032.029  000.774: opening buffers
032.136  000.017  000.017: sourcing /Users/jinnouchi.yasushi/.local/share/nvim/site/pack/packer/start/direnv.vim/autoload/direnv/extra_vimrc.vim
032.151  000.106: BufEnter autocommands
032.153  000.002: editing files in windows
```

`--- NVIM STARTING ---` から起動プロセスが始まり、`editing files in windows` が完了、つまりカーソルを動かしたりキーが反応するタイミングです。この例では起動に **32 ms** 掛かっていることになります。

## 2.2. rhysd/vim-startuptime コマンド

`--staruptime` オプションで出力されるログでは起動時に読み込まれるファイル全てについて記載されています。これはこれで、細かく処理に掛かった時間が分かって便利なのですが、いくつか問題があります。

* 試すたびに値が結構変わる。
* パッと見て、起動速度が分かりづらい。
  - すでに起動してるのに `editing files in windows` 以降もログが出たりします（ステータスラインの描画に掛かった部分など）。

これを解決するため、単純に複数回実行して平均値を取るユーティリティがあります。[rhysd/vim-startuptime][] です。

[rhysd/vim-startuptime]: https://github.com/rhysd/vim-startuptime

```sh
$ vim-startuptime -vimpath nvim -count 1000
Extra options: []
Measured: 1000 times

Total Average: 29.251860 msec
Total Max:     45.369000 msec
Total Min:     28.025000 msec

……（以下、各種ファイルの読み込みに掛かった時間がソートされて出る）……
```

この例では 1000 回実行して平均値を計算しています。便利ですね。

## 2.3. dstein64/vim-startuptime プラグイン

こちらは外部コマンドではなくてプラグインです。このプラグインをインストールすると (Neo)Vim 上で `:StartupTime` コマンドが使えるようになり、起動する際に各ファイルの読み込みに掛かった時間が分かります。

<img alt="スクリーンショット 2022-11-18 7.35.21.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/8880/5f886380-e41f-ed5f-e78e-8fd33d77cb39.png" width="524">

こんな感じで、綺麗なグラフと共に結果が表示されるので分かり易いです。ただ、“percent” の欄は合計値が 100 にならないことに注意しましょう。この例で言うと、`init.lua` → `core` → `_compiled` の順にそれぞれのファイルで `require` してますので、掛かった時間は包含されています。

# 3. この記事で達成する目標

さて、皆さんの (Neo)Vim がどの程度の速度で起動するか測ってみたでしょうか。マシンスペックやインストールしているプラグインの数、`init.lua` の大きさにも依りますので一概には言えませんが、感覚としては大体以下のように分類できるでしょう。

<dl>
<dt>500 ms 以上</dt>
<dd>このレベルだと明らかに遅さを実感します。プラグインてんこ盛りの上、遅延読み込み（後述）していないとこうなってしまいます。これでは「サッと起動してすぐに編集できる」とはお世辞にも言えない状態です。<dd>
<dt>100 ms～500 ms</dt>
<dd>あまりプラグインを入れていない、又は、適度にプラグインを遅延読み込みさせるとこの程度の速度になると思います。頻繁に使ってもそんなに苦にならない速度ですが、偶に素の (Neo)Vim を起動したりすると速度の違いに驚いたりします。</dd>
<dt>50 ms～100 ms</dt>
<dd>遅延読み込みについてかなり詳しい人でもこの程度が限度ではないでしょうか。</dd>
<dt>50 ms 以下</dt>
<dd>ここまで来ると素の (Neo)Vim との差はほとんど感じられなくなります。この記事ではこの速度を目標とします。</dd>
</dl>

素の、何もプラグインがなく、何も設定ファイルに書いていない (Neo)Vim が、そりゃ一番速いです。とはいえ快適に編集したいなら誰しもある程度プラグインをインストールしたり設定ファイルに追記したりするでしょう。この記事では僕が必要と思うプラグイン（現時点で **159 個**）入れた状態で **29 ms** の起動速度を実現した設定を元に、高速化の秘訣を書いていきます。

:::note info

[ayamir/nvimdots][] は「最速で 10 ms で起動する」ことを謳う設定例ですが、あくまでこれはマシンスペックや OS にも依ります。僕の環境では ayamir/nvimdots は **26 ms** で起動するのがせいぜいでした。そのため僕自身の設定で **29 ms** というのはほぼ理想に近い値と言えます。

:::

# 4. プラグインの遅延読み込み

何はともあれ、プラグインを遅延読み込みさせる方法を学びましょう。ここでは遅延読み込み自体の方法、依存関係の解決、複雑な設定例について述べます。

## 4.1. 遅延読み込みの種類

[packer.nvim][]においては、何も指定せずにプラグイン名を追加すると start プラグインとして認識され、Neovim 起動時に読み込まれるようになります。

```lua
use { "vim-jp/vimdoc-ja" }
```

これに対し、いくつかのオプションを指定するとそれを opt プラグインとして配置し、起動後に必要に応じて読み込めるようになります。

:::note info

「start プラグイン」「opt プラグイン」の違いについては [Vim のヘルプ][packages]、あるいは[前回の記事][]を参照してください。

[packages]: https://vim-jp.org/vimdoc-ja/repeat.html#packages

:::

| オプションの例 | 意味 |
|--|--|
| `opt = true` | opt プラグインとして配置するが読み込みタイミングは特に指定しない。 |
| `cmd = { "StartupTime" }` | ex コマンドを呼び出すと読み込む。 |
| `ft = { "perl" }` | 指定されたファイルタイプのファイルを開くと読み込む。 |
| `keys = { { "n", "'j" } }` | キーマッピングで読み込む。 |
| `event = { "BufRead" }` | イベントで読み込む。 |
| `fn = { "searchx#*" }` | Vimscript 関数を呼び出すと読み込む。 |
| `module = { "neodev" }` | Lua モジュールを `require` すると読み込む。 |
| `module_pattern = { "plenary.*" }` | `module` は `module_pattern` の糖衣構文です。 |

大体は読んだ通りなのですが、`opt` オプションだけはちょっと特殊です。これは依存プラグインとして列挙するだけで、実際の読み込みタイミングは後述の `after` や `wants` で指定するものに利用します。

## 4.2. 依存関係や読み込み順序の指定

一部のプラグインには依存関係があり、読み込み順序の指定が必要な場合があります。ただ、packer.nvim のこの部分はあまり整理されておらず、似たような機能を持つオプションが 3 つもあります。

### 4.2.1. `requires` オプション

**依存関係**を指定するオプションです。[inkarkat/vim-LineJuggler][] というプラグインがあります。プラグイン自体については[大昔に記事を書いた][juggling]のでそっち読んで貰うとして、こいつの特徴は依存するプラグインが多いことです。下記の設定では依存する 3 つのプラグインも一緒にインストールされます。この際、特に遅延読み込みの設定をしていないので、全て start プラグインとして配置しています。

[inkarkat/vim-LineJuggler]: https://github.com/inkarkat/vim-LineJuggler
[juggling]: https://blog.delphinus.dev/2012/10/juggling-in-vim.html

```lua
use {
  "inkarkat/vim-LineJuggler",
  requires = {
    { "inkarkat/vim-ingo-library" },
    { "tpope/vim-repeat" },
    { "vim-scripts/visualrepeat" },
  },
}
```

重要なのはこれ、**依存関係**なんですね。**読み込み順序**ではないんです。遅延読み込みではこの二種の概念を整理しないと上手く動かないんです。

問題は、これを遅延読み込みさせる時です。

```lua
use {
  "inkarkat/vim-LineJuggler",
  keys = { { "n", "[e" }, { "n", "]e" } },
  requires = {
    { "inkarkat/vim-ingo-library" },
    { "tpope/vim-repeat" },
    { "vim-scripts/visualrepeat" },
  },
}
```

`keys` オプションを加えることで、該当のキーを押すまでプラグインの読み込みを遅延させたとします。この場合、inkarkat/vim-LineJuggler は想定通り opt プラグインとなりますが、依存する 3 つのプラグインは start プラグインのままです。

```lua
use {
  "inkarkat/vim-LineJuggler",
  keys = { { "n", "[e" }, { "n", "]e" } },
  requires = {
    { "inkarkat/vim-ingo-library", opt = true },
    { "tpope/vim-repeat", opt = true },
    { "vim-scripts/visualrepeat", opt = true },
  },
}
```

じゃあ依存プラグインも opt にして該当のキーを押した時に連続して読み込ませよう、とすると、これは上手く行かず、エラーになってしまいます。この例で言うと、`[e` を押した時は inkarkat/vim-LineJuggler のみが読み込まれ、依存する 3 つのプラグインは読み込まれません。これは**依存関係**だけ定義されて、**読み込み順序**は未定義だからなんですね。

これを解決するには後述の **after** オプションか、**wants** オプションを使います。

### 4.2.2. `after` オプション

:::note warn

最初に書いておきますが、 **`after` オプションは問題が多いので使わない方がいいです。** なのに先にこれを例示しているのは、次項の `wants` オプションがドキュメントに書かれていない、隠しオプションだからです。「packer.nvim のこの辺はあまり整理されていない」というのはこれが理由です。

:::


上述の例は以下のように `after` オプションを指定することで上手く行きます。

```lua
use {
  "inkarkat/vim-LineJuggler",
  keys = { { "n", "[e" }, { "n", "]e" } },
  requires = {
    { "inkarkat/vim-ingo-library", opt = true, after = "vim-LineJuggler" },
    { "tpope/vim-repeat", opt = true, after = "vim-LineJuggler" },
    { "vim-scripts/visualrepeat", opt = true, after = "vim-LineJuggler" },
  },
}
```

しかしこれ……端的に言って見辛くないですか？　普通に英語として読むと、“after vim-LineJuggler” は「inkarkat/vim-LineJuggler の後に読み込む」のようになると思うんですが、実際には inkarkat/vim-LineJuggler の**前**に読み込まれるんです。

`after` オプションとできることはほぼ同じなのですが、次項の `wants` オプションの方が記法として素直なのでお勧めです。

### 4.2.3. `wants` オプション

`wants` オプションを使うとこれがすっきりします。

```lua
use {
  "inkarkat/vim-LineJuggler",
  keys = { { "n", "[e" }, { "n", "]e" } },
  requires = {
    { "inkarkat/vim-ingo-library", opt = true },
    { "tpope/vim-repeat", opt = true },
    { "vim-scripts/visualrepeat", opt = true },
  },
  wants = { "vim-ingo-library", "vim-repeat", "visualrepeat" },
}
```

読んだまま、分かり易いですね！　先に書いた通り、`wants` オプションは隠しオプションなのですが、多くの人に使われているので素直にこれ使っちゃいましょう。

## 4.3. お勧めの遅延読み込みパターン

いくつかのプラグインについて遅延読み込みの実例を見てみましょう。

### 4.3.1. `keys` オプションの例

すでに例示した inkarkat/vim-LineJuggler はプラグイン自体でキーマッピングを定義してくれます。

1. `[e` を押す。
2. inkarkat/vim-LineJuggler とその依存プラグインが読み込まれ、`map [e` も正しく定義される。
3. 期待した動作が行われる。

このような順序で処理されるのですが、では、マッピングをユーザーが自前で行うプラグインの場合はどうでしょう？ [junegunn/vim-easy-align][] というプラグインを例として考えます。このプラグインはヘルプを読むと、`<Plug>(EasyAlign)` を呼び出すマッピングを自前で定義するように書かれています。

[junegunn/vim-easy-align]: https://github.com/junegunn/vim-easy-align

```vim
xmap ga <Plug>(EasyAlign)
nmap ga <Plug>(EasyAlign)
```

この設定はどこに書くべきでしょうか？　もちろん `init.lua` に直に書いてもいいんですが、プラグインの設定は一箇所に纏めたいですよね？　このような場合、`setup` オプションを使うと綺麗に書けます。

```lua
use {
  "junegunn/vim-easy-align",
  keys = { "<Plug>(EasyAlign)" },
  setup = function()
    vim.keymap.set("x", "ga", "<Plug>(EasyAlign)")
    vim.keymap.set("n", "ga", "<Plug>(EasyAlign)")
  end,
}
```

`setup` オプションに指定した関数は Neovim 起動時に実行されます。そのため、

1. （起動時）`ga` というマッピングを定義する。
2. `ga` を押す。
3. `<Plug>(EasyAlign)` が呼ばれる。
4. `vim-easy-align` が読み込まれる。
5. 期待した動作が行われる。

というピタゴラスイッチが機能するわけです。

### 4.3.2. `module` オプション、`module_pattern` オプションの例

最近の Lua 製のプラグインは `<Plug>(Hoge)` みたいなマッピングを余り定義しません。Neovim では直接 Lua の関数をマッピングする方が見た目も素直ですからね。こういう時は `keys` よりも `module` / `module_pattern` で書いた方が綺麗になります。

[numToStr/FTerm.nvim][] というプラグインがあります。Floating Windows でいい感じにターミナルが開けるプラグインなのですが、デフォルトでは特にマッピングを提供していません。例えば、`<A-c>` というマッピングでターミナルを開閉する時は以下のように定義します。

[numToStr/FTerm.nvim]: https://github.com/numToStr/FTerm.nvim

```lua
use {
  "numToStr/FTerm.nvim",
  module = { "FTerm" },
  setup = function()
    vim.keymap.set("n", "<A-c>", function()
      require("FTerm").toggle()
    end)
    vim.keymap.set("t", "<A-c>", function()
      require("FTerm").toggle()
    end)
  end,
  config = function()
    require("Fterm").setup {
      -- 様々な設定
    }
  end,
}
```

1. （起動時）`<A-c>` というマッピングを定義する。
2. `<A-c>` を押す。
3. 定義した関数が実行され `FTerm` というモジュールが `require` される。
4. numToStr/FTerm.nvim プラグインが読み込まれ、`config` オプションに定義した関数が実行される。
5. 期待した動作（`toggle()` 関数の呼び出し）が行われる。

:::note info

存在しないモジュールを `require` した時に期待するプラグインを読み込むことができるのは、packer.nvim によって `package.loaders` が[上書きされている][]からです。

[上書きされている]: https://github.com/wbthomason/packer.nvim/blob/master/lua/packer/compile.lua#L144-L169

:::

プラグイン自体の設定は `config` オプションの関数に纏め、`setup` オプションでキーマッピングを定義しています。役割が分けられていて読み易いです。

`module` オプションは、実は正規表現版の `module_pattern` オプションの糖衣構文となっています。上記の `module = { "Fterm" }` というのは `module_pattern = { "^Fterm" }` という設定と同じです。

### 4.3.3. 起動後すぐに必要ないものは `event` オプションで遅延読み込み

<!--
すぐに思い付くのはファイルタイププラグインです。特定のファイルタイプだけで必要なプラグインはそのファイルを開いた時に読み込みたいです。

```lua
use { "vim-perl/vim-perl", ft = { "perl", "perl6" } }
```

QuickFix ウィンドウを拡張する [kevinhwang91/nvim-bqf][] も、ファイルタイプ `qf` なウィンドウですから `ft` オプションが使えます。

[kevinhwang91/nvim-bqf]: https://github.com/kevinhwang91/nvim-bqf

```lua
use { "kevinhwang91/nvim-bqf", ft = { "qf" } }
```
-->

これは色んなパターンがありますね。起動時すぐには要らないけれど、ふと気付いたらあって欲しいというものがあります。例えば、[b0o/incline.nvim][] はウィンドウ毎にファイル名などを表示するプラグインですが、気付いた時にあればいい、程度のものです。以下のようにすると、カーソルが暫く止まった時、あるいは、別のアプリケーションやペイン（tmux を使っている場合）に切り替えた時に読み込みます。

[b0o/incline.nvim]: https://github.com/b0o/incline.nvim

```lua
use {
  "b0o/incline.nvim",
  event = { "FocusLost", "CursorHold" },
  config = function()
    require("incline").setup {}
  end,
}
```

起動時には必要なく、ファイルを開いた時にだけ必要なものもありますね。[ahmedkhalf/project.nvim][] はファイルを開くと自動的にカレントディレクトリを移動してくれるプラグインですが、Neovim 起動時はまだファイルを開いてないので移動する必要がありません。

[ahmedkhalf/project.nvim]: https://github.com/ahmedkhalf/project.nvim

```lua
use {
  "ahmedkhalf/project.nvim",
  event = { "BufRead", "BufNewFile" },
  config = function()
    require("project_nvim").setup {}
  end,
}
```

[petertriho/nvim-scrollbar][] は Neovim にスクロールバーを追加してくれるプラグインです。これはプラグインのソースを読むと実際に描画すべきイベントが[定義されています][scrollbar-event]。これに応じて遅延読み込みするのが最善です。

[petertriho/nvim-scrollbar]: https://github.com/petertriho/nvim-scrollbar
[scrollbar-event]: https://github.com/petertriho/nvim-scrollbar/blob/dd0adc741c2b0a5cbd52e4888aa46e4ab3f53cb1/lua/scrollbar/config.lua#L95-L104

```lua
use {
  "petertriho/nvim-scrollbar",
  event = {
    "BufWinEnter",
    "CmdwinLeave",
    "TabEnter",
    "TermEnter",
    "TextChanged",
    "VimResized",
    "WinEnter",
    "WinScrolled",
  },
  config = function()
    require("scrollbar").setup {}
  end,
}
```

後はコマンドラインでのみ意味のあるプラグイン（例えば [houtsnip/vim-emacscommandline][]）は `CmdlineEnter` で読み込むとか、挿入モードでのみ意味のあるプラグイン（例えば [delphinus/skkeleton_indicator.nvim][]）は `InsertEnter` で読み込むとか、本当に様々なパターンが考えられます。Neovim で[定義されている][events]イベントは多数ありますから一度一覧を見てみると良いでしょう。

[houtsnip/vim-emacscommandline]: https://github.com/houtsnip/vim-emacscommandline
[delphinus/skkeleton_indicator.nvim]: https://github.com/delphinus/skkeleton_indicator.nvim
[events]: https://neovim.io/doc/user/autocmd.html#autocmd-events

## 4.4. 複雑な遅延読み込み

依存関係や設定の都合上、特殊な遅延読み込みを行うパターンもあります。少し雑多な内容ですがここに纏めておきます。

### 4.4.1. hrsh7th/nvim-cmp

Neovim 界で補完プラグインのデファクトは [hrsh7th/nvim-cmp][] でしょう。この種のプラグインは往々にして依存プラグインが多く、設定も複雑になりがちです。補完プラグインは `InsertEnter` イベントで読み込むのがセオリーなのですが、依存する複数のプラグイン全てをこのイベントで遅延読み込みさせようとすると、これは上手く行きません。

```lua
-- 上手く行かない例
use {
  "hrsh7th/nvim-cmp",
  event = { "InsertEnter" },
  requires = {
    { "hrsh7th/cmp-buffer", event = { "InsertEnter" } },
    { "hrsh7th/cmp-emoji", event = { "InsertEnter" } },
    -- ……以下各種ソースプラグインが続く
  },
  config = function()
    require("cmp").setup {
      -- 各種設定
    }
  end
}
```

なぜ上手く行かないかと言うと、`event` オプションによる読み込みでは順序が前後する可能性があるのからです。ソースプラグイン内で `require "cmp"` した時に「そんなモジュールはないよ」と言われてしまうことがあるんですね。

各種ソースプラグインには幸い、遅延読み込みに使い易い一つの共通点があります。各々のソースプラグインは必ず `after/plugin/cmp_hoge.lua` というファイルを含んでおり、このファイルはプラグイン追加時に必ず呼ばれます。そしてその中で `require "cmp"` しているんですね。

```lua
-- 上手く遅延読み込みできる例
use {
  "hrsh7th/nvim-cmp",
  module = { "cmp" },
  requires = {
    { "hrsh7th/cmp-buffer", event = { "InsertEnter" } },
    { "hrsh7th/cmp-emoji", event = { "InsertEnter" } },
    -- ……以下各種ソースプラグインが続く
  },
  config = function()
    require("cmp").setup {
      -- 各種設定
    }
  end
}
```

1. `i` などで挿入モードに入る。
2. `InsertEnter` イベントが発生。
3. 各種ソースプラグインが読み込まれる。
4. `after/plugin/cmp_hoge.lua` 内で `require "cmp"` が呼ばれる。
5. hrsh7th/nvim-cmp が読み込まれる。
   - ここで `config` オプションに指定した関数も実行されます。
6. 期待した動作が行われる。

:::note info

一部、`after/plugin/cmp_hoge.lua` を含んでいないソースプラグインもあるようです。これが hrsh7th/nvim-cmp 的に正しい実装なのかは分からないですが、このようなソースプラグインを使う時はこれだけを start プラグインとして配置するといいかも知れません。

:::

### 4.4.2. nvim-telescope/telescope.nvim

こちらも Neovim 界では一番人気のファジーファインダー、[nvim-telescope/telescope.nvim][] です。こちらはそれほど複雑な設定にはならず、`wants` オプションなどを適切に指定すれば OK です。ファジーファインダーでは良くあることなのですが、マッピングなどはユーザー個々の好き嫌いがありますので、ここに挙げる設定はあくまで参考程度にしてください。

```lua
use {
  "nvim-telescope/telescope.nvim",
  module = { "telescope" },
  requires = {
    { "nvim-telescope/telescope-ghq.nvim", opt = true },
    { "nvim-telescope/telescope-z.nvim", opt = true },
    -- その他の拡張プラグイン……
  },
  wants = {
    "telescope-ghq.nvim",
    "telescope-z.nvim",
    -- ……
  },
  setup = function()
    local function builtin(name)
      return function(opt)
        return function()
          return require("telescope.builtin")[name](opt or {})
        end
      end
    end

    local function extensions(name, prop)
      return function(opt)
        return function()
          local telescope = require "telescope"
          telescope.load_extension(name)
          return telescope.extensions[name][prop](opt or {})
        end
      end
    end

    vim.keymap.set("n", "<Leader>f:", builtin "command_history" {})
    vim.keymap.set("n", "<Leader>fG", builtin "grep_string" {})
    vim.keymap.set("n", "<Leader>fH", builtin "help_tags" { lang = "en" })
    vim.keymap.set("n", "<Leader>fm", builtin "man_pages" { sections = { "ALL" } })
    vim.keymap.set("n", "<Leader>fq", extensions("ghq", "list") {})
    vim.keymap.set("n", "<Leader>fz", extensions("z", "list") {})
    -- ……以降設定が続く
  end,
  config = function()
    require("telescope").setup {
      -- 様々な設定
    }
  end
}
```

1. `<Leader>fH` を押す。
2. `builtin` 関数が呼び出され、中で `require "telescope.builtin"` される。
3. nvim-telescope/telescope.nvim が読み込まれる。
   - 上述したように、`module = { "telescope" }` は `^telescope` という正規表現でモジュール読み込みにフックします。そのため、`telescope.builtin` という指定でプラグインが読み込まれるのです。
4. 期待した動作が行われる。

ここでは `builtin` や `extensions` と言った便利関数を用意してマッピングを読み易くしています。Lua では関数呼び出しの括弧を省けるので、DSL のように書けて良いですね。

### 4.4.3. folke/noice.nvim

[folke/noice.nvim]: https://github.com/folke/noice.nvim

:::note info

[folke/noice.nvim][] は開発の活発なプラグインですので、ここに書いた設定もすぐに陳腐化するかも知れません。ここに載せたものはあくまで執筆当時に利用できた例として読んでください。

:::

folke/noice.nvim は `set cmdheight=0` と共に使うことを意図した、Neovim の UI 全般を改善するプラグインです。このプラグインには様々な機能があります。例えばコマンドラインへのメッセージ表示にフックして起動し、メッセージを OS の通知センターのように画面右上に表示する形にしてくれます。`set cmdheight=0` はコマンドライン自体を全く表示しないようにするオプションですから、folke/noice.nvim はほとんど必須と言っていいプラグインです。

未だ不完全なのですが、folke/noice.nvim を遅延読み込みする例を以下に載せます。

```lua
use {
  "folke/noice.nvim",
  event = { "BufRead", "BufNewFile", "InsertEnter", "CmdlineEnter" },
  module = { "noice" },
  requires = {
    { "MunifTanjim/nui.nvim" },
    {
      "rcarriga/nvim-notify",
      module = { "notify" },
      config = function()
        require("notify").setup {
          -- nvim-notify の設定
        }
      end
    },
  },
  wants = { "nvim-treesitter" },
  setup = function()
    if not _G.__vim_notify_overwritten then
      vim.notify = function(...)
        local arg = { ... }
        require "notify"
        require "noice"
        vim.schedule(function()
          vim.notify(unpack(args))
        end)
      end
      _G.__vim_notify_overwritten = true
    end
  end,
  config = function()
    require("noice").setup {
      -- noice.nvim の設定
    }
  end
}
```

folke/noice.nvim は [MunifTanjim/nui.nvim][], [rcarriga/nvim-notify][] という 2 つのプラグインに依存しています。後者がコマンドラインのメッセージを OS の通知センターのように表示する役目を負っています。folke/noice.nvim が難しいのは、通常の遅延読み込みフックである `event` や `module` 以外に、「`vim.notify` が呼ばれた時」に起動する必要があることです。

[MunifTanjim/nui.nvim]: https://github.com/MunifTanjim/nui.nvim
[rcarriga/nvim-notify]: https://github.com/rcarriga/nvim-notify

[vim.notify][] は `:echo` や `:echomsg` に代わるメッセージ表示 API です。上記の `setup` オプションではこれを上書きし、次のようにして folke/noice.nvim に処理をバトンタッチしています。

[vim.notify]: https://neovim.io/doc/user/lua.html#vim.notify()

1. Neovim 起動時、`setup` オプションに指定された関数が呼ばれます。
2. `vim.notify` を上書きします。中では rcarriga/nvim-notify, folke/noice.nvim を `require` しますが、それぞれ読み込み契機として `module = { "noice.nvim" }` のように指定しています。これにより、プラグインが読み込まれると同時にそれぞれの `config` オプションに設定された関数も呼ばれます。
3. `require("noice").setup {}` の中では、`vim.notify` が folke/noice.nvim のものに置き換わります（実際の処理は[ここ][]）。
4. 最後に、上書きされた `vim.notify` で自身を呼び出して完了です。
   - `vim.notify` の上書きは `vim.schedule` を使ってイベントループに[スケジューリングされています][vim.schedule]。そのため、ここでの呼び出しも `vim.schedule` で遅らせないと効果がありません。

[ここ]: https://github.com/folke/noice.nvim/blob/3489e57e198e4b161169a538d2bd71e018de41d0/lua/noice/init.lua#L63-L63
[vim.schedule]: https://github.com/folke/noice.nvim/blob/6824794d01fb27e428f642440517fe066bf47105/lua/noice/init.lua#L42-L42

遅延読み込みしたい場合はこのように、プラグインの内部実装についても詳しく調べる必要があるのです。

:::note info

folke/noice.nvim は他のメッセージ表示コマンド（`echo`, `echomsg`, `confirm` などなど）についてもフックして起動します。今回 `vim.notify` だけを上書きしているのは、僕の環境ではこれが folke/noice.nvim 自身より前に呼ばれる可能性の高いものだったからです。プラグイン構成によっては起動時に `echo` を呼ぶ場合もあると思います。そういう場合にこれは上手く働きませんので folke/noice.nvim は遅延読み込みしない方が良いでしょう。

:::

### 4.4.4. vim-denops/denops.vim を使ったプラグインを遅延読み込みする

[vim-denops/denops.vim]: https://github.com/vim-denops/denops.vim

[vim-denops/denops.vim][] は TypeScript を使ったプラグインエコシステムです。このプラグインを利用すると、TypeScript で Neovim / Vim 両対応のプラグインが書けます。開発ツールも豊富な TypeScript でプラグインを書けるとあって、（特に日本で）広く受け入れられています。有名なものとして、以下のようなプラグインが vim-denops/denops.vim を利用して開発されています。

* [Shougo/ddc.vim](https://github.com/Shougo/ddc.vim)
* [Shougo/ddu.vim](https://github.com/Shougo/ddu.vim)
* [vim-skk/skkeleton](https://github.com/vim-skk/skkeleton)
* [yuki-yano/fuzzy-motion.vim](https://github.com/yuki-yano/fuzzy-motion.vim)

最初に言っときますと、**packer.nvim では vim-denops/denops.vim を使ったプラグインの遅延読み込みをサポートしていません。**

:::note info

vim-denops/denops.vim を使ったプラグインを遅延読み込みする時は `packadd` するだけではダメなのです。Deno プロセス自体を起動したり、プラグインを指定して関数を呼び出す必要があります。これに対応したプラグインマネージャーとしては [Shougo/dein.vim][] が挙げられます。

[Shougo/dein.vim]: https://github.com/Shougo/dein.vim

:::

これには一応解決法もありまして（というか僕が作ったんですが）packer.nvim に対して PR があります。

* [Load plugins using denops.vim lazily by delphinus · Pull Request #755 · wbthomason/packer.nvim](https://github.com/wbthomason/packer.nvim/pull/755)

```lua
-- denops.vim を使ったプラグインを遅延読み込みする例
use {
  -- fork した packer.nvim を使います。
  { "delphinus/packer.nvim", branch = "feature/denops", opt = true },

  -- カーソルが暫く動かなかった時や、別のウィンドウ・ペインに移動した時に
  -- 裏で読み込んでおきます。
  { "vim-denops/denops.vim", event = { "CursorHold", "FocusLost" } },

  {
    "vim-skk/skkeleton",
    wants = { "denops.vim" },
    keys = { { "i", "<Plug>(skkeleton-toggle)" } },
    setup = function()
      vim.keymap.set("i", "<C-j>", "<Plug>(skkeleton-toggle)")
    end
    config = function()
      vim.fn["skkeleton#config"] {
        -- ……様々な設定……
      }
    end
  },
}
```

僕自身も使っていて特に問題は起きていないのですが、如何せん packer.nvim ユーザーには余り需要がないのか、マージされる気配がありません。みなさんから wbthomason さん（作者）にマージして！と言っていただけると早くマージされるかも知れません。

### 4.4.5. packer.nvim 自身を遅延読み込みする

packer.nvim は Neovim の [packages 機能][packages]を利用するためのユーティリティ集と言っていい存在です。そのため、一度プラグイン構成を決めてしまうとそれ以上読み込んでおく必要はありません。この辺は[以前の記事][]にも書いたので詳しい話は省略します。

[以前の記事]: https://qiita.com/delphinus/items/8160d884d415d7425fcc

```lua
use {
  "wbthomason/packer.nvim",
  module = { "packer" },
  setup = function()
    local packer
    local function run_packer(method)
      return function(opts)
        if not packer then
          vim.cmd.packadd "packer.nvim"
          packer = require "packer"
          packer.init {
            -- 様々な設定
          }
          packer.use {
            -- 様々なプラグイン
          }
        end
      end

      packer[method](opts)
    end

    vim.api.nvim_create_user_command("PackerInstall", run_packer "install", { desc = "[Packer] Install plugins" })
    vim.api.nvim_create_user_command("PackerUpdate", run_packer "update", { desc = "[Packer] Update plugins" })
    vim.api.nvim_create_user_command("PackerClean", run_packer "clean", { desc = "[Packer] Clean plugins" })
    vim.api.nvim_create_user_command("PackerStatus", run_packer "status", { desc = "[Packer] Output plugins status" })
    vim.api.nvim_create_user_command("PackerCompile", run_packer "compile", { desc = "[Packer] Output plugins status" })
    vim.api.nvim_create_user_command("PackerSync", run_packer "sync", { desc = "[Packer] Update plugins" })
    -- …… 以下、他のコマンドも定義する
  end,
}
```

`setup` オプションで設定していますのでこれらのコマンドは Neovim 起動時に定義されます。ですが、その時点で `packer.nvim` は読み込まれていません。例えばプラグインの更新のために `:PackerSync` とコマンドを打ったとします。するとその時に初めて `packer.nvim` の設定が行われます。普通に Neovim で作業している間は読み込まれないのです。

# 5. ファイルタイププラグインの扱い

さて、ここまで長々と「プラグインの遅延読み込み」について述べてきました。ここからは遅延読み込みに直接関わらない、Neovim 起動ステップの高速化について見ていきます。

## 5.1. ファイルタイププラグインとは？

[ファイルタイププラグイン][]とは、特定のファイルタイプ（例えば `c`, `perl` , `dockerfile`）のファイルを開いた時に行われる専用の設定（構文ハイライトやインデントなど）を纏めたものです。ファイルタイププラグインはその「専用の設定」とは別に、「どうやってファイル（正確にはバッファー）をそのファイルタイプと認識するのか」という仕組みがあります。

[ファイルタイププラグイン]: https://vim-jp.org/vimdoc-ja/usr_43.html#43.1

例えば、[delphinus/vim-firestore][] というプラグインがあります。[Firestore Security Rules][] を書く時に便利なものですが、このプラグインには `ftdetect/firestore.vim` というファイルが含まれています。

[delphinus/vim-firestore]: https://github.com/delphinus/vim-firestore
[Firestore Security Rules]: https://firebase.google.com/docs/firestore/security/get-started

```vim:ftdetect/firestore.vim
autocmd BufNewFile,BufRead *.rules set filetype=firestore
```

中にはこんな感じで自動コマンドが定義されており「これらの拡張子ならファイルタイプを `firestore` にする」と書かれています。

## 5.2. ファイルタイププラグインを遅延読み込みする

ここで重要なことがあります。この仕組みは当然ですが、ファイルを読み込む前にこの設定がないと、そもそも `firestore` として認識されません。つまりこれに従うなら、delphinus/vim-firestore は Neovim 起動時に読み込んでおかないと意味を為さないことになってしまいます。

これは初期の [packer.nvim][] においても同様で、[前回の記事][]でも初稿では「ファイルタイププラグインは遅延読み込みできない」としていました。その後、packer.nvim も改善されて制限は無くなっています。例えば以下のようにファイルタイププラグインを遅延読み込みした場合、

[前回の記事]: https://qiita.com/delphinus/items/8160d884d415d7425fcc

```lua
use {
  "delphinus/vim-firestore",
  ft = { "firestore" },
}
```

コンパイルした設定ファイルには次のように追記されます。

```lua
-- augroup 開始
vim.cmd [[augroup filetypedetect]]
-- プロファイリング用関数
time([[Sourcing ftdetect script at: /Users/jinnouchi.yasushi/.local/share/nvim/site/pack/packer/opt/vim-firestore/ftdetect/firestore.vim]], true)
-- ftdetect スクリプトを読み込む
vim.cmd [[source /Users/jinnouchi.yasushi/.local/share/nvim/site/pack/packer/opt/vim-firestore/ftdetect/firestore.vim]]
-- augroup 終了
vim.cmd("augroup END")
```

コメントは全部僕が付けたものです。このように、Neovim 起動時にプラグイン全体を読み込むことなく、`ftdetect` スクリプトのみを先に読み込んでいるのです。

## 5.3. `ftdetect` スクリプトを自作する

上記の方式は単純に `ftdetect` スクリプトを読み込んでいるだけなので、ファイルタイププラグインの数が増えていくと段々起動速度も遅くなります。これを解決するには自分で `ftdetect` スクリプトを 1 ファイルに纏めると良いです。

今回の例の場合、自分の設定ディレクトリに作ったファイルにそのまま内容をコピペするか、

```vim:~/.config/nvim/ftdetect/mine.vim
" vim-firestore/ftdetect/firestore.vim
autocmd BufNewFile,BufRead *.rules set filetype=firestore
" その他のファイルタイププラグインについてもコピペしていく……
```

この際ですから Lua のコードに変換してしまうのも手ですね。

```lua:~/.config/nvim/ftdetec/mine.lua
-- vim-firestore/ftdetect/firestore.vim
vim.filetype.add {
  extension = {
    rules = "firestore",
    -- ……以下続く……
  },
  -- 自分独自の設定を追加できるのも強みです。
  pattern = {
    ["%.rules$"] = function(_, _, _)
      local is_exist = vim.loop.fs_stat ".gcloudignore"
      if is_exist then
        vim.opt.filetype = "firestore"
      end
    end
  },
}
-- その他のファイルタイププラグインについても変換していく……
```

:::note info

`vim.filetype` は最近追加されたファイルタイプ判別用の API です。詳しくは[ヘルプ][]を読んでください。

[ヘルプ]: https://neovim.io/doc/user/lua.html#lua-filetype

:::

その上で、コンパイルした packer.nvim の設定ファイルからは読み出し部を消してしまいましょう。`PackerCompile` の後に手で消してもいいですが、どうせなら自動化してみます。

```lua
vim.api.nvim_create_autocmd("User", {
  pattern = "PackerCompileDone",
  callback = function()
    -- これはデフォルトのファイル名です。packer.nvim のオプションで指定できます。
    local compile_path = "~/.config/nvim/plugin/packer_compiled.lua"
    vim.cmd("split ++enc=latin1 " .. compile_path)
    -- この行から
    vim.cmd [[/^vim\.cmd \[\[augroup filetypedetect\]\]$]]
    -- この行までを削除します。
    vim.cmd [[.,/^vim\.cmd("augroup END")$/delete]]
    vim.cmd.wq { bang = true }
    vim.notify("Successfully edited " .. vim.fs.basename(compile_path))
  end
})
```

`PackerCompileDone` は `PackerCompile` コマンドの実行直後に発火するイベントです。コンパイル後のファイルに対してここで後処理を加えることができます。

:::note info

この、「`ftdetect` スクリプトを一つのファイルに纏める」というのは [Shougo/dein.vim][] では標準で対応してくれる機能です。packer.nvim ではこのようなチューニングは自分でやらないといけません。

:::

## 5.4. そもそもファイルタイププラグインを使わない

前段を読んで「なんだかめんどくさいなあ。これそこまでして使う必要ある？」と思った方、正解です。実は、ファイルタイププラグインは現代の Neovim ではそこまで必須のものではなくなっています。これは以下の理由に寄るものです。

### 5.4.1. Tree-sitter の存在

[Tree-sitter][] は元々 [Atom][] の一機能として開発されましたが、Neovim ではこれを本体に組み込むことで従来の正規表現ベースだった構文ハイライトを改善しています。現時点でも多くのファイルタイプが Tree-sitter による構文解析に対応しており、有効なファイルタイプについては従来の（Vimscript で書かれた）`syntax` ファイルは利用されません。マイナーなファイルタイプ（例えば拙作 [delphinus/vim-firestore][]）のためには相変わらず必要ですが、ある程度有名なファイルタイプのために専用のプラグインは不要かも知れません。

[Tree-sitter]: https://tree-sitter.github.io/tree-sitter/
[Atom]: https://atom.io

### 5.4.2. 標準で認識できるファイルタイプが増えた

Neovim だけでなく Vim もですが、標準で認識できるファイルタイプはどんどん増えています。以前はファイルタイププラグインが必要だったファイルタイプでも `ftdetect` スクリプトのためだけに導入していたとしたら必要無くなっている可能性もあります。

もっとも、インデントや `compiler` 設定など、Neovim 本体にはない設定もあるでしょう。もう一度ファイルタイププラグインの機能を確認し、本当に必要か見定めた方が良いです。

### 5.4.3. LSP や整形ツールによっても代替できる

ファイルタイププラグインの機能は LSP によって代替できるものも多いです。例え対応する Language Server がインストールできなくても、先に挙げたインデント機能などは整形ツールをファイル保存時に実行するだけで十分かも知れません。例えば、Lua ファイルを書く時は Neovim 本体のインデント機能よりも、ファイル保存後に [JohnnyMorganz/StyLua][] を実行する方が遥かに綺麗になるでしょう。

[JohnnyMorganz/StyLua]: https://github.com/JohnnyMorganz/StyLua

:::note info

LSP についてはこの記事では深く触れないことにします。上記のように整形ツールを連携して使う際は [jose-elias-alvarez/null-ls.nvim][] の利用を検討すると良いでしょう。

[jose-elias-alvarez/null-ls.nvim]: https://github.com/jose-elias-alvarez/null-ls.nvim

:::

プラグインには様々な副作用があります。不要なプラグインを設定に残しておくのはトラブルの元です。。是非これを機に自分の使っているプラグインリストを整理してみましょう。

# 6. 高速なカラースキームを使う

カラースキームについても述べておかなければなりません。カラースキームはそれこそ星の数ほど存在し、ユーザーの環境によって最適なものも異なります。個人の好みも千差万別ですからどれが良いというのもありません。

ただ、Neovim の起動を高速化するに当たって、考慮しておくべきポイントがあります。

## 6.1. Tree-sitter の要素を考慮しているかどうか

前述しました通り、現代的な Neovim の構文ハイライトは Tree-sitter と切り離して語ることはできません。Tree-sitter により解析されたノードに対し、以下のようにハイライトを定義することができます。

```lua
vim.api.nvim_set_hl(0, "@boolean", { fg = "#81A1C1", bold = true })
vim.api.nvim_set_hl(0, "@constant.builtin" { fg = "#8FBCBB", bold = true })
vim.api.nvim_set_hl(0, "@constant.macro", { fg = "#8FBCBB", bold = true })
vim.api.nvim_set_hl(0, "@error", { fg = "#BF616A" })
vim.api.nvim_set_hl(0, "@exception", { fg = "#B48EAD" })
vim.api.nvim_set_hl(0, "@function.macro", { fg = "#8FBCBB" })
-- ……以下沢山の定義が続く……
```

従来の正規表現ベースのハイライトと違って細かく指定することでコードを分かり易く表示できます。Neovim では（正確には [nvim-treesitter/nvim-treesitter][] プラグインにより）標準的なハイライトを定義しています。そのためカラースキーム側の対応は必須ではないのですが、やはり細かく指定して提供されている方が良いでしょう。

[nvim-treesitter/nvim-treesitter]: https://github.com/nvim-treesitter/nvim-treesitter

## 6.2. 古い不要な定義が残っていないか

Tree-sitter が無い時代に定義されたハイライトがカラースキームにそのまま残っている場合があります。例えば Neovim の `runtime/syntax/markdown.vim` には以下のように `markdownH1`～`markdownH6` という構文リージョンを定義しています（[ソースへのリンク][]）。

[ソースへのリンク]: https://github.com/neovim/neovim/blob/master/runtime/syntax/markdown.vim#L77-L82

```vim:runtime/syntax/markdown.vim
syn region markdownH1 matchgroup=markdownH1Delimiter start=" \{,3}#\s"      end="#*\s*$" keepend oneline contains=@markdownInline,markdownAutomaticLink contained
syn region markdownH2 matchgroup=markdownH2Delimiter start=" \{,3}##\s"     end="#*\s*$" keepend oneline contains=@markdownInline,markdownAutomaticLink contained
syn region markdownH3 matchgroup=markdownH3Delimiter start=" \{,3}###\s"    end="#*\s*$" keepend oneline contains=@markdownInline,markdownAutomaticLink contained
syn region markdownH4 matchgroup=markdownH4Delimiter start=" \{,3}####\s"   end="#*\s*$" keepend oneline contains=@markdownInline,markdownAutomaticLink contained
syn region markdownH5 matchgroup=markdownH5Delimiter start=" \{,3}#####\s"  end="#*\s*$" keepend oneline contains=@markdownInline,markdownAutomaticLink contained
syn region markdownH6 matchgroup=markdownH6Delimiter start=" \{,3}######\s" end="#*\s*$" keepend oneline contains=@markdownInline,markdownAutomaticLink contained
```

これを利用したカラースキームとしては [morhetz/gruvbox][] が挙げられます。このプラグインでは実際に `markdownH1`～`markdownH6` に違ったハイライトを[設定しています][]。

[morhetz/gruvbox]: https://github.com/morhetz/gruvbox
[設定しています]: https://github.com/morhetz/gruvbox/blob/bf2885a95efdad7bd5e4794dd0213917770d79b7/colors/gruvbox.vim#L1332-L1337

```vim
hi! link markdownH1 GruvboxGreenBold
hi! link markdownH2 GruvboxGreenBold
hi! link markdownH3 GruvboxYellowBold
hi! link markdownH4 GruvboxYellowBold
hi! link markdownH5 GruvboxYellow
hi! link markdownH6 GruvboxYellow
```

しかしこれらの構文リージョンに対するハイライトは、Tree-sitter を有効にしている場合全て無視されてしまいます。カラースキームはその性質上、Neovim 起動時に必ず読み込むものですから、利用しない構文ハイライトまで定義すると起動が遅くなってしまいます。できればここまで考慮したカラースキームを選びたいですね。

:::note info

ここでは Tree-sitter の詳しい使い方は説明しません。上述のようにタイトル行のレベルに応じてハイライトを変えたい場合は以下のような記事・プラグインを参照してください。

* [日常に彩りを加える nvim-treesitter の設定術](https://zenn.dev/monaqa/articles/2021-12-22-vim-nvim-treesitter-highlight)
* [lukas-reineke/headlines.nvim](https://github.com/lukas-reineke/headlines.nvim)

:::

## 6.3. Lua で書いたカラースキームってどう？速いの？

他のプラグインと同様、カラースキームの中には完全に Lua で書かれたものも存在します（[folke/tokyonight.nvim][] などが有名でしょうか）。Lua で書かれてるのだから Vimscript で書かれた従来のものより起動が早くなる……かと思いきや、実はそんなに違いがありません。

[folke/tokyonight.nvim]: https://github.com/folke/tokyonight.nvim

Vimscript と Lua で差が出るのは、頻繁に呼ばれるコードや `runtimepath` の探索部分です。カラースキームの読み込みは API をひたすら呼び出す作業ですし、条件分岐などのロジックもほとんどありません。あくまで「不要なハイライトまで定義していないか」が速度に影響します。

Lua で書かれたカラースキームが明らかに優れていると思うのはその保守性・可読性です。ハイライトをモジュールに分けて管理し、その単位で機能をオン・オフすることができたりします。コントリビュートする際もやり易いですね。

## 6.4. ……なんか難しいな。結局どのカラースキームがいいの？

と思われたでしょうが、こればっかりは好みがありますのでどれが良いともお勧めすることができません。一つ言えるのは、「カラースキームは自作するのが最善」であることです。完全にゼロから作るのは難しいですが、公開されているものをカスタマイズして自分専用のカラースキームを作ってみるのが良いでしょう。

:::note info

僕自身は [delphinus/nord-nvim][] という、[arcticicestudio/nord-vim][] をカスタマイズしたものを使っています。好みの設定を適当に追加・削除したものなので、ドキュメントなどは書いておりません ;)

[delphinus/nord-nvim]: https://github.com/delphinus/nord-nvim
[arcticicestudio/nord-vim]: https://github.com/arcticicestudio/nord-vim

:::

## 6.5. `ColorScheme` イベント

プラグインの管理と関連するところでは、`ColorScheme` イベントの利用が挙げられます。例えば [lewis6991/gitsigns.nvim][] を使う場合、それに関連したハイライトはプラグインを追加する際纏めて書いておきたいです。

[lewis6991/gitsigns.nvim]: https://github.com/lewis6991/gitsigns.nvim

```lua
use {
  "lewis6991/gitsigns.nvim",
  event = { "FocusLost", "CursorHold" },
  setup = function()
    vim.api.nvim_create_autocmd("ColorScheme", {
      callback = function(opts)
        if opts.match == "nord" then
          vim.api.nvim_set_hl(0, "GitSignsAdd", { fg = "#a3be8c" })
          vim.api.nvim_set_hl(0, "GitSignsChange", { fg = "#ebcb8b" })
          vim.api.nvim_set_hl(0, "GitSignsDelete", { fg = "#bf616a" })
        else
          -- ……他のカラースキームの場合の設定……
        end
      end
    })
  end,
  config = function()
    require("gitsign").setup {
      -- 様々な設定
    }
  end,
}
```

特にカラースキームを自作している場合は、カラースキーム自身にプラグイン向けのハイライトを定義したくなります。しかしそれだと、将来プラグインを使わなくなった時に消し忘れることもあるでしょう。この例のように、プラグインを使わなくなった時に纏めて消せるようにしておくと間違いがありません。遅延読み込みする場合は `config` オプションでプラグイン読み込み後に指定する手もありますが、このように `ColorScheme` イベントで定義しておくと Neovim 起動中にカラースキームを変えた場合でも適切に設定されます。

:::note info

自動コマンドの `callback` に関数を設定した場合は引数から様々な情報を受け取ることができます。[`nvim_create_autocmd` のドキュメント][]や [`ColorScheme` イベントのドキュメント][]を参照してください。

[`nvim_create_autocmd` のドキュメント]: https://neovim.io/doc/user/api.html#nvim_create_autocmd()
[`ColorScheme` イベントのドキュメント]: https://neovim.io/doc/user/autocmd.html#ColorScheme

:::

# 7. ステータス行・タブページ行を遅延読み込みする

これは Neovim の見た目や使い勝手にも影響しますので禁じ手に近いやり方です。ですが、ステータス行・タブページ行の描画が重い場合には大変効果があります。

```lua
use {
  "nvim-lualine/lualine.nvim",
  event = { "InsertEnter", "CursorHold", "FocusLost", "BufRead", "BufNewFile" },
  requires = {
    { "kyazdani42/nvim-web-devicons", module = { "nvim-web-devicons" } },
  },
  setup = function()
    vim.opt.laststatus = 0
    vim.opt.showtabline = 0
  end
  config = function()
    require("lualine").setup {
      -- ……様々な設定……
    }
  end
}
```

[nvim-lualine/lualine.nvim][] は Neovim 界でステータス行・タブページ行をカスタマイズするプラグインのデファクトです。これは人に寄ると思いますが、僕の場合ステータス行・タブページ行を見るのは実際に編集を開始した後で、Neovim を起動後にファイルを開くまでは必要ありません。そのため、`setup` オプションに指定した関数で描画自体を無くしています。その後、挿入モードに入った時（`InsertEnter`）や、ファイルを開いた時（`BufRead` など）、あるいは編集する手を止めた時（`CursorHold` など）でプラグインを読み込むようにしているのです。

これは、僕がいつも Neovim を `nvim` のように、ファイルを開かずに起動しているから使える手です。`nvim filename.txt` のように直接ファイルを開いて使うような人には余り効果が無いかも知れませんね。

# 8. 更に上を目指すには

さて、ここまでで Neovim の起動を高速化するテクニックはあらかた網羅できたと思います。しかし当初の話題に戻ると、僕の設定では平均起動速度は **29 ms** です。[ayamir/nvimdots][] を僕のマシンで動かした時は **26 ms** 程度で起動していました。後はどのようなことで差が出ているのでしょうか。

## 8.1. 利用するプラグインの数を減らす

単純ですが、これが一番大きな問題です。僕は常に 160 個程度のプラグインを使っていますが、ayamir/nvimdots はその半分ほどしかプラグインを読み込みません。もちろんほとんどは遅延読み込みさせているのですが、その設定にもコードが必要です。例えば以下のような単純なプラグイン設定を追加したとしましょう。

```lua
use {
  "petertriho/nvim-scrollbar",
  event = { "BufWinEnter" },
  config = function()
    require("scrollbar").setup {}
  end,
},
```

これにより、コンパイルしたファイルには以下のように追記されます。

```lua
_G.packer_plugins = {
  ["nvim-scrollbar"] = {
    config = { "\27LJ\2\n;\0\0\3\0\3\0\a6\0\0\0'\2\1\0B\0\2\0029\0\2\0004\2\0\0B\0\2\1K\0\1\0\nsetup\14scrollbar\frequire\0" },
    loaded = false,
    needs_bufread = false,
    only_cond = false,
    path = "/Users/jinnouchi.yasushi/.local/share/nvim/site/pack/packer/opt/nvim-scrollbar",
    url = "https://github.com/petertriho/nvim-scrollbar"
  },
  -- ……他のプラグイン設定が続きます……
}

-- このイベントでプラグインを読み込みます。
vim.cmd [[au BufWinEnter * ++once lua require("packer.load")({'nvim-scrollbar'}, { event = "BufWinEnter *" }, _G.packer_plugins)]]
```

Lua という言語は非常に高速ですが、Neovim の API を呼び出す際はそれなりの時間が掛かります。自動コマンド（`au BufWinEnter`）が積もり積もれば差は無視できません。例え遅延読み込みしたとしても起動速度への影響はゼロにならないのです。

## 8.2. 設定自体を簡潔にする

僕の Neovim の画面は以下のようになっています。

![スクリーンショット 2022-11-13 17.00.09.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/8880/1ead7f23-cc6c-d70d-8a5c-2b35ecfbf8eb.png)

ayamir/nvimdots と比べてまあ見るからに複雑になっています（ステータス行・タブページ行は遅延読み込みしていますが）。見えない所では [nvim-telescope/telescope.nvim][] や [hrsh7th/nvim-cmp][] についても関連するプラグイン・設定が多くありますので、勢いコンパイルされた時のコード量も増えてしまうのです（例えば nvim-telescope/telescope.nvim の設定は [370 行もあります][]）。これは簡略化しようと思えばできるんでしょうが、これこそ好みの問題ですので難しいです。

[370 行もあります]: https://github.com/delphinus/dotfiles/blob/master/.config/nvim/lua/modules/telescope/config.lua#L2-L372

---

というわけで、これ以上の高速化を望むのは自らが Neovim に望むこととのトレードオフになってきます。ayamir/nvimdots でさえ、素の Neovim には及ばないのですからね。自分なりに納得できるポイントを定めて、そこに向かって高速化を目指しましょう。

:::note info

Neovim を ayamir/nvimdots 以上に速く起動させることはできるのでしょうか？　実はその方法があります。Neovim の `runtime` ファイル（標準で添付されたスクリプト群）には様々なプラグインを含んでいます。

```sh
# Neovim 内のターミナルで試してみましょう
$ ls -l $VIMRUNTIME/plugin
```

この他、起動プロセスで必ず読み込まれるファイルがいくつかありますので、それらを物理的に削除してしまえば良いのです。どのようなファイルがそれに当たるのかは「2.1. `--staruptime` オプション」のログを確認してみましょう。

これらは正に禁じ手で、行為の意味を把握せずに誤ってファイルを削除してしまうと Neovim が起動しなくなります。気を付けて実践しましょう。

:::

# 9. おまけ - ayamir/nvimdots を試してみよう

最後におまけとして、超高速な設定ファイル集、[ayamir/nvimdots][] を自分の環境で試してみましょう。↓単純に以下のようにするだけで起動はするのですが、注意しないと環境を汚してしまいます。

```sh
$ cd /tmp
$ git clone https://github.com/ayamir/nvimdots
$ cd ~
# 自分の設定を退避
$ mv .config/nvim{,.orig}
# ayamir/nvimdots をリンク
$ ln -s /tmp/nvimdots .config/nvim
```

このままだと、起動時に [fatih/vim-go][] の色んなツールをインストールしてしまいますし、[Shada][] など重要なファイルも上書きしてしまいます。

[fatih/vim-go]: https://github.com/fatih/vim-go
[Shada]: https://neovim.io/doc/user/starting.html#shada-file

以下ではこれを避け、普段使っている Neovim 環境を汚さない方法を書いています。

:::note info

ayamir/nvimdots を気軽に試せる `Dockerfile` も[書いてみました][]。これを使うと次のコマンドで ayamir/nvimdots を使った Neovim が起動できます。

[書いてみました]: https://gist.github.com/delphinus/afe3d94cab35f55bb1d4f79e74245cc5

```sh
mkdir /tmp/hoge
cd /tmp/hoge
curl -LO https://gist.githubusercontent.com/delphinus/afe3d94cab35f55bb1d4f79e74245cc5/raw/6d3917a8e2740845a35567fad8bbd6c80be0bb50/Dockerfile
docker build -t nvimdots .
docker run -it nvimdots

nvim
```

ただ、起動速度を比較する場合は Docker 上だと余り意味がありませんから、以下に示した方法で行った方が良いですね。

:::

先ずは、ayamir/nvimdots 内でプラグインが関連ファイルをインストールするのを止めます。

```sh
# run = ... 行を全てコメントアウト
$ cd /tmp/nvimdots
$ perl -i -pe 's/^(?=\s*run =)/--/' **/*.lua
```

[Shada][] やインストール済みのプラグインがあるディレクトリを退避します。

```sh
$ cd ~
$ mv .local/share/nvim{,.orig}
$ mv .local/state/nvim{,.orig}
```

これでやっと起動できます。

```sh
$ mv .config/nvim{,.orig}
$ ln -s /tmp/nvimdots .config/nvim
$ nvim
```

初回起動時はプラグインのインストールなどが始まるので適当に `<Enter>` を押してください。何回か Neovim 自体を起動・終了させる必要があるかも知れません。上手く起動できたら起動速度も測ってみましょう。みなさんの起動速度とどのくらいの差があるでしょうか？

```sh
$ vim-startuptime -vimpath nvim -count 1000
```

最後に、元通りに戻しておきましょう。

```sh
$ rm .config/nvim
$ rm -fr .local/share/nvim
$ rm -fr .local/state/nvim
$ mv .config/nvim{.orig,}
$ mv .local/share/nvim{.orig,}
$ mv .local/state/nvim{.orig,}
```