A few months ago I changed my neovim configuration and started using the Lazy package manager. The thing is that Lazy is fantastic at installing plugins. LSP, neotest, lazydev, telescope and all the goodies from the wonderful neovim community are easy to install and add.

So it is not surprising that I ended up with 37 plugins installed and loaded every time I run nvim. While none of the plugins are particularly heavy and the startup time is below 100 ms. It is still interesting to find out why is the package manager is named Lazy.

Make Lazy.nvim lazy again

folke/lazy.nvim already advertises the lazy loading feature

  • πŸ”Œ Automatic lazy-loading of Lua modules and lazy-loading on events, commands, filetypes, and key mappings

The problem is that most of the configuration examples do not care. After all, loading on startup doesn’t break anything, so it is a good default after all. This leaves the exercise of lazily loading stuff to the curious user.

Lazy itself provides all the necessary hints. Just type :Lazy and check the output. In my case nvim loads 5 mandatory plugins, tht can’t be postponed without breaking the editor.

  Total: 37 plugins

  Loaded (5)
    ● gruvbox 3.57ms ξ«“ start
    ● lazy.nvim 5.56ms ο„‘ init.lua
    ● lualine.nvim 6.19ms ξͺ† VeryLazy
    ● nvim-lastplace 0.57ms ξ«“ start
    ● vim-sleuth 0.47ms ξ«“ start

VeryLazy

That is actually a cool name. In short, lazy allows you to start a plugin in a case of an event. VeryLazy is specific one. Documentation says

VeryLazy: triggered after LazyDone and processing VimEnter auto commands

VeryLazy plugins are not counted in the startup time. Just press P in :Lazy view to compare lualine.nvim 3.82ms ξͺ† VeryLazy

  Startuptime: 36.28ms

  Based on the actual CPU time of the Neovim process till UIEnter.
  This is more accurate than `nvim --startuptime`.
    LazyStart 14.69ms
    LazyDone  28.89ms (+14.2ms)
    UIEnter   36.28ms (+7.39ms)


    ● ξ«“ startuptime 36.28ms
    ● ξͺ† VeryLazy 5.33ms
      ➜ ο’‡ lualine.nvim 4.06ms

And as lualine.nvim 4.11ms ξ«“ start

  Startuptime: 47.56ms

  Based on the actual CPU time of the Neovim process till UIEnter.
  This is more accurate than `nvim --startuptime`.
    LazyStart 12.86ms
    LazyDone  37.18ms (+24.32ms)
    UIEnter   47.56ms (+10.38ms)

    ● ξ«“ startuptime 47.56ms
    ● ξͺ† VeryLazy 1.56ms

VeryLazy is then the simplest optimization. But the plugin will still load on every startup. So it is indeed a simple but lazy optimization. With a cool name.

Events

Lazy Loading offers an excellent place to start. Unfortunately a bit short on examples.

event: Lazy-load on event. Events can be specified as BufEnter or with a pattern like BufEnter *.lua

The value can be a string, an array of strings, or a function or a patterns that allows you to start the plugin after some action. Here are gitsigns and nvim-lint plugins configured to get loaded after a buffer is read. As neither is language dependent, there isn’t much to do.

  β—‹ gitsigns.nvim ξͺ† BufReadPost 
  β—‹ nvim-lint ξͺ† BufReadPost ξͺ† InsertLeave ξͺ† BufWritePost 

~/.config/nvim/lua/plugins/gitsigns.lua contains

return {
  "lewis6991/gitsigns.nvim",
  event = { "BufReadPost" },
}

File types

I tend to use a language agnostic tools, so have a lspconfig/neotest/conform combo instead of go.nvim. Nothing against go.nvim, it is a great plugin I use as a base for my configuration. This means that most of the plugins I have can’t be restricted to a file type. There are notable exceptions though.

  β—‹ kulala.nvim ο€– http 
  β—‹ lazydev.nvim ο€– lua 
  β—‹ luvit-meta 

Lazydev is for Lua files, and since it’s a plugin from folke himself, it would be odd to not to suggest a lazy loading by default.

return {
  {
    "folke/lazydev.nvim",
    -- only load on lua files
    ft = "lua",
  },
  { -- optional `vim.uv` typings
    "Bilal2453/luvit-meta",
    lazy = true
  },
}

lazy=true options is handy for library plugins. Such plugin will be loaded when needed by other plugin.

Command

Since I use some commands only via neovim’s command line, there’s nothing easier than loading the plugin after typing it on a command line. This is a case of vim-fugitive and :G command. Lazy registers the command and loads the plugin when it is really needed.

  β—‹ vim-fugitive ξ―‡ G 
return {
  "tpope/vim-fugitive",
  cmd = "G",
}

Keys

Some other plugins have a defined shortcut(s) to activate. In my case it’s the powerful Telescope plugin. Lazy can also handle this scenario too. The plugin can be activated with a command or a shortcut. This form expects the actual keymap to be activated inside config function of a plugin.

  β—‹ neotest ο„œ <leader>t 
  β—‹ telescope.nvim ξ―‡ Telescope ο„œ <leader>sr ο„œ <leader>s.
return {
  {
    "nvim-telescope/telescope.nvim",
    cmd = "Telescope",
    keys = {
      {"<leader><leader>"},
      {"<leader>sh"},
    },
    config = function()
      -- set <leader>sh here
      vim.keymap.set("n", "<leader>sh", ...)
    end
  },
}

Conclusion

Was it worth it? I reduced the startup time of an empty Neovim from 90ms to ~40ms, where opening a Go file increases the time to 80ms. So in terms of numbers it was not. On the other hand, it was cool and fun, and I hope you’ve enjoyed reading this.

Fonts

FiraCode Nerd Font under SIL OPEN FONT LICENSE Version 1.1, converted to woff2 format by cloudconvert.com