lazy.nvimの遅延読み込みとopts・config・initの使い分け
lazy.nvimの設定にcmdやkeysを書けば、そのプラグインは使うときまで読み込まれない。そう理解して書いた設定でも、起動直後に:Lazyを開くと、遅らせたはずのプラグインがLoadedの側に並んでいることがあります。書いた引き金が無視されたわけではありません。別の経路から先に呼ばれています。
この記事では、遅延読み込みがどの条件で効き、どんな書き方で黙って崩れるのかを、一時ディレクトリに作った設定で1つずつ確かめました。後半では、毎回迷うopts・config・initの違いを、同じプラグインを2つのファイルに書いたときのマージまで含めて実測しています。
検証環境:NVIM v0.12.6(macOS)、lazy.nvim v11.17.5。HOME・XDG_CONFIG_HOME・XDG_DATA_HOMEなどを一時ディレクトリに向け、普段の設定に触れない状態で試しました。読み込み状態はlazy.nvimが持つ各プラグインの_.loadedを出力して確かめ、:Lazyの画面はtmuxの中で開いて写しています。画面のアイコン(Nerd Fontの字形)は表示できないため省きました。起動時間はnvim --headless --startuptimeを21〜25回ずつ取った中央値です。
目次(9項目)
lazy.nvimを入れる最小の構成
lazy.nvimはプラグインマネージャ自身をgitで取ってくるところから始まります。init.luaに書くのは次の数行で、空の環境で起動すると、lazy.nvimのクローンとlua/plugins/に書いたプラグインのインストールまでが初回の起動で済みました(手元では約4秒)。
-- ~/.config/nvim/init.lua
vim.g.mapleader = " "
local lazypath = vim.fn.stdpath("data") .. "/lazy/lazy.nvim"
if not vim.uv.fs_stat(lazypath) then
vim.fn.system({ "git", "clone", "--filter=blob:none", "--branch=stable",
"https://github.com/folke/lazy.nvim.git", lazypath })
end
vim.opt.rtp:prepend(lazypath)
require("lazy").setup({ spec = { { import = "plugins" } } })
{ import = "plugins" }は、lua/plugins/の下の.luaファイルを全部読み、それぞれが返すテーブルをプラグインの一覧(spec)として扱う指定です。mapleaderをsetup()より前に置いているのは、keysに書いた<leader>がキーを割り当てた時点の値で展開されるからです。lazy.nvimのソースにも「vim.g.mapleaderはlazyを読み込む前に設定する」という警告文があります。
プラグインの置き場所は~/.local/share/nvim/lazy/で、Vimの標準パッケージやvim-plugのpack/やplugged/とは別です(Vimにプラグインを入れる方法)。
遅延読み込みが効く条件(event / cmd / keys / ft)
lazy.nvimの既定は「すぐ読み込む」です。何も書かなければ起動時に読み込まれ、次の4つのどれかを書いたときだけ、その引き金が引かれるまで後回しになります。試した設定はこれです。
-- lua/plugins/base.lua
return {
{ "folke/which-key.nvim", event = "VeryLazy", opts = {} },
{ "tpope/vim-fugitive", cmd = "Git" },
{ "nvim-mini/mini.surround", keys = { "sa" }, opts = {} },
{ "preservim/vim-markdown", ft = "markdown" },
{ "folke/trouble.nvim", cmd = "Trouble", opts = {} },
{ "folke/todo-comments.nvim", dependencies = { "nvim-lua/plenary.nvim" }, opts = {} },
}
Luaファイルを開いて起動し、すぐに:Lazyを開いた画面です。右端の語が「何によって読み込まれたか」、Not Loadedの側では「何を待っているか」を示します。
Total: 8 plugins
Loaded (4)
● lazy.nvim 4.31ms init.lua
● plenary.nvim 0.27ms todo-comments.nvim
● todo-comments.nvim 1.54ms start
● which-key.nvim 0.97ms VeryLazy
Not Loaded (4)
○ mini.surround sa
○ trouble.nvim Trouble
○ vim-fugitive Git
○ vim-markdown markdown
このあと:e a.mdでvim-markdownがmarkdownの印付きで読み込まれ、:Git --versionでvim-fugitiveがGit、ノーマルモードでsaiw)を打った時点でmini.surroundがsaの印付きでLoadedへ移りました。saは引き金になったうえで、そのまま囲む操作としても効きました。lazy.nvimは仮のキー割り当てを置き、押されたらプラグインを読み込んでから同じキーを送り直すからです。
event = "VeryLazy"だけは性質が違います。何かの操作を待つのではなく、起動が終わって画面が出たあとに読み込みます。上の画面でwhich-key.nvimがすでにLoadedなのはそのためで、起動時間の計測からは外れますが、画面が出た直後には読み込まれています。--headlessで起動するとVeryLazyは来ず、not loadedのままでした。
遅延したつもりで起動時に読み込まれる3つのパターン
ここからが本題です。引き金を書いたのに起動時に読み込まれてしまう形を、実際に作って:Lazyで見ました。
別プラグインのdependenciesに入っている
trouble.nvimにはcmd = "Trouble"を書いたまま、todo-comments.nvimのdependenciesにtrouble.nvimを足しました。todo-comments.nvimには引き金が無いので起動時に読み込まれ、依存として並べたものも道連れになります。
Loaded (4)
● lazy.nvim 10.7ms init.lua
● plenary.nvim 0.25ms todo-comments.nvim
● todo-comments.nvim 8.5ms start
● trouble.nvim 7.02ms todo-comments.nvim
trouble.nvimの右端がTroubleではなくtodo-comments.nvimになっています。trouble.nvim側のspecだけ読めば遅延しているように見え、原因は別のファイルにあるのが厄介です。
逆向きも確かめました。todo-comments.nvim自身にcmd = "TodoQuickFix"を付けると、起動直後はtodo-comments.nvim・plenary.nvim・trouble.nvimの3つともnot loadedになり、:TodoQuickFixを実行した時点で3つまとめて読み込まれました。依存だけが先に読み込まれることはなく、親が読み込まれる時点で一緒に読み込まれます。依存させるなら親のほうも遅延させます。起動時間への影響はこの組み合わせで中央値26.8msから28.6msと小さいものでしたが、依存に入れたものが重ければそのぶんが乗ります。
initでrequireしている
initは起動時に必ず実行される関数です。ここでrequire("trouble")を呼ぶと、lazy.nvimはモジュールが要求された時点でプラグインを読み込むので、cmdを書いていても起動時に読み込まれます。
{ "folke/trouble.nvim", cmd = "Trouble", opts = {},
init = function() require("trouble") end },
Loaded (2)
● lazy.nvim 3.61ms init.lua
● trouble.nvim 1.56ms trouble plugins.base
右端はtrouble(要求されたモジュール名)とplugins.base(要求した設定ファイル)です。同じinitでも、中身がvim.keymap.set("n", "<leader>xx", "<cmd>Trouble diagnostics toggle<cr>")のようにコマンドを呼ぶキー割り当てだけなら、trouble.nvimはnot loadedのままでした。
自分の設定ファイルの先頭でrequireしている
3つ目はinitに限らない話です。lua/keymaps.luaの先頭にlocal trouble = require("trouble")と書き、init.luaからrequire("keymaps")すると、trouble.nvimはkeymaps.luaから要求されたものとして起動時に読み込まれました。同じキー割り当てをfunction() require("trouble").toggle("diagnostics") endのように関数の中でrequireする形に変えると、読み込みはキーを押すまで起きません。requireを書く位置が、ファイルの先頭か関数の中かで結果が分かれます。
optsとconfigとinitの違い
3つは呼ばれる時点と役割が違います。mini.surroundにlazy = falseを付けて、setup()が呼ばれたかどうか(MiniSurroundが存在するか)と、設定値n_lines(既定20)を見ました。
| 書き方 | setup | n_lines |
|---|---|---|
opts = { n_lines = 50 } | 呼ばれる | 50 |
opts = {} | 呼ばれる | 20 |
config = true | 呼ばれる | 20 |
| どちらも書かない | 呼ばれない | なし |
opts+config関数(setupを書き忘れ) | 呼ばれない | なし |
opts+config関数でsetup(opts) | 呼ばれる | 50 |
opts = {}とconfig = trueは結果が同じで、どちらもrequire("mini.surround").setup({})相当が呼ばれました。読み込みのタイミングにも差はありません。どちらも書かないとsetup()は呼ばれず、mini.surroundのようにsetup()で初めて動くプラグインはキーも割り当てられません。
落とし穴はconfigを関数で書いたときです。関数の第2引数にoptsが渡ってくるので、中でprint(vim.inspect(opts))すると{ n_lines = 50 }が出ます。ところが自分でsetup(opts)を呼ばない限り、その値はどこにも当たりません。config関数は「setup()の自動呼び出しを自分の処理で置き換える」指定だからです。
{ "nvim-mini/mini.surround", opts = { n_lines = 50 },
config = function(_, opts)
require("mini.surround").setup(opts) -- これを書かないと opts は捨てられる
-- setup のあとにやりたい処理をここに足す
end },
initはこの2つと実行される時点が違い、プラグインを読み込む前の起動時に毎回走ります。vim.gの変数のように、読み込み前に決まっていないといけない値を置く場所で、ここでrequireすると遅延が崩れます。
同じプラグインを2つのファイルに書いたとき
LazyVimのように元の設定を別ファイルで上書きする使い方では、同じプラグインのspecが複数できます。plugins/base.luaとplugins/extra.luaに分けて書き、optsとconfigがどう合わさるかを見ました(importはファイル名順に読むので、extraが後になります)。
| base.lua | extra.lua | 結果 |
|---|---|---|
n_lines = 50, mappings = { add = "ys" } | mappings = { delete = "ds" } | n_lines=50、add=ys、delete=ds |
custom_surroundingsに( | custom_surroundingsに[ | 両方残る |
list = { "a", "b" } | list = { "c" } | { "c" }(置き換え) |
n_lines = 50 | optsを関数にしてopts.n_lines * 2 | n_lines=100 |
config関数でsetup(opts) | 別のconfig関数 | extraだけ実行、setupされない |
テーブルのoptsは入れ子のキーまで深くマージされますが、配列は継ぎ足されずに後のファイルの値で置き換わりました。配列に足したいときはoptsをfunction(_, opts) table.insert(opts.list, "c") endのような関数にすると、前のファイルまでのマージ結果に"c"が足されて{ "a", "b", "c" }になりました。configは合わさらず、後に書いたほうだけが実行されます。親のconfigがsetup(opts)を呼んでいても、上書きした側のconfigが呼ばなければsetup()は実行されません。optsで書ける設定はoptsに寄せておくと、上書きしたときに壊れにくくなります。
:Lazy profileで起動時間を測る
:Lazy profile(:Lazyの画面でP)は、起動の内訳をプラグインごとに出します。冒頭の設定で開いた画面の上半分です。
Startuptime: 38.12ms
Based on the actual CPU time of the Neovim process till UIEnter.
This is more accurate than `nvim --startuptime`.
LazyStart 16.21ms
LazyDone 26.01ms (+9.8ms)
UIEnter 38.12ms (+12.11ms)
Profile
● lazy.nvim 6.28ms
➜ module 1.43ms
➜ config 0.3ms
➜ spec 3.16ms
● startup 8.05ms
➜ start 1.62ms
★ start todo-comments.nvim 1.6ms
‒ plenary.nvim 0.26ms
startの下に並ぶのが起動時に読み込まれたプラグインで、依存はその子として字下げされます。Ctrl-sで時間のかかった順に並べ替えられるので、重いものから疑えます。
遅延あり・なしの差はnvim --startuptimeでも比べました。同じ6本を、上の書き方のまま、全部lazy = falseにしたもの、プラグイン0本(lazy.nvimだけ)の3通りで、25回ずつ交互に起動した中央値です。
| 設定 | 起動時間(中央値) |
|---|---|
| プラグイン0本 | 26.3ms |
| 遅延あり(起動時はtodo-commentsとplenaryだけ) | 29.5ms |
| 全部すぐ読み込む | 39.7ms |
6本でも10ms差が出て、遅延ありの設定は0本との差が3ms程度に収まりました。--headlessなのでVeryLazyのぶんは入っていません。:Lazy profileはUIEnterまでのCPU時間で測り方が違うため、前後を比べるときはどちらかにそろえます。
lazy-lock.jsonでバージョンを固定する
インストールや更新のたびに、lazy.nvimは~/.config/nvim/lazy-lock.jsonを書き出します。記録されるのはプラグインごとのブランチとコミットだけで、lazy.nvim自身も含まれます。
{
"lazy.nvim": { "branch": "main", "commit": "85c7ff3711b730b4030d03144f6db6375044ae82" },
"vim-fugitive": { "branch": "master", "commit": "3b753cf8c6a4dcde6edee8827d464ba9b8c4a6f0" }
}
設定ディレクトリの中にあるので、dotfilesと一緒にgitで管理すれば別のマシンでも同じコミットをそろえられます。mini.surroundのコミットを手で10個前のもの(a2f644f)に書き換えて:Lazy restoreを実行すると、プラグインのディレクトリはHEAD detached at a2f644fになりました。その後:Lazy update mini.surroundで最新(5357f00)に進み、ロックファイルの値も書き換わりました。更新して何かが壊れたときは、gitでロックファイルを前のコミットに戻してから:Lazy restoreすれば、更新前の状態に戻れます。
vim.packに移るか、lazy.nvimに残るか
Neovim 0.12には標準のプラグイン管理vim.packが入りました。:help vim.pack.add()を読むと、vim.pack.add()は呼ばれた時点でインストールと:packaddを済ませる関数で、eventやcmdのような引き金の指定はありません。loadに関数を渡せますが、キーやコマンドで後回しにする仕組みは自分で書くことになります。
遅延読み込み、specのマージ、:Lazy profileの内訳表示は、lazy.nvimに残る理由になります。LazyVimを使っているなら土台がlazy.nvimなので、そのまま残るのが自然です(kickstart.nvimとLazyVimの選び方)。プラグインが数本で起動時間に困っていない、外部のツールを減らしたい、という場合はvim.packへ移る意味があります。移ったあとで構成に問題が無いかは:checkhealthで確かめられます。
trouble.nvimをtodo-comments.nvimの依存に入れる形は、実験用に作ったつもりで、書いている途中で自分の設定にも似た行があるのに気づきました。cmdを付けたことで安心して、:Lazyを開いても右端の語まで読んでいなかったのです。もう1つ戸惑ったのは、--headlessでwhich-key.nvimがいつまでもnot loadedのままだったことで、最初はevent = "VeryLazy"の書き方を間違えたのだと思い込み、tmuxで画面を出して起動し直すまで原因が分かりませんでした。
まとめ
lazy.nvimは何も書かなければ起動時に読み込み、event・cmd・keys・ftのどれかを書いたときだけ後回しにします。後回しにしたつもりでも、即時に読み込まれる親のdependenciesに入っている、initでrequireしている、自分の設定ファイルの先頭でrequireしている、の3つでは起動時に読み込まれました。確かめるには:Lazyを開いて、各プラグインの右端に引き金の語が出ているかを見ます。
設定はoptsに書けばsetup(opts)が自動で呼ばれ、configを関数にしたら自分で呼びます。分けて書いたoptsは深くマージされますが、配列は置き換わり、configは後勝ちです。