← Vim研究所

lazy.nvimの遅延読み込みとopts・config・initの使い分け

2026-10-28 公開 ・

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)を見ました。

書き方setupn_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.luaextra.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 = 50optsを関数にしてopts.n_lines * 2n_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は後勝ちです。

関連記事

← Vim研究所 トップへ戻る