lazy.nvim can make your Neovim start faster than you thought possible, even with dozens of plugins.
Here’s a peek at lazy.nvim in action, managing a typical setup. Imagine this is your ~/.config/nvim/lua/plugins.lua:
return {
-- UI
{
"nvim-treesitter/nvim-treesitter",
build = ":TSUpdate",
event = { "BufReadPost", "BufNewFile" }, -- Load on file events
opts = {
ensure_installed = { "vim", "lua", "vimdoc", "query" },
highlight = { enable = true },
indent = { enable = true },
},
},
{
"nvim-telescope/telescope.nvim",
tag = "0.1.5",
dependencies = { "nvim-lua/plenary.nvim" },
cmd = "Telescope", -- Load only when Telescope command is called
opts = {
defaults = {
layout_strategy = "horizontal",
layout_config = { preview_width = 0.5 },
sorting_strategy = "ascending",
winblend = 0,
},
pickers = {
find_files = {
hidden = true,
},
},
},
},
{
"nvim-lualine/lualine.nvim",
event = "VimEnter", -- Load when Vim starts
opts = {
options = {
icons_enabled = true,
theme = "auto",
component_separators = { left = "", right = "" },
section_separators = { left = "", right = "" },
disabled_filetypes = {
statusline = {},
winbar = {},
},
ignore_focus = {},
always_divide_backend = true,
globalstatus = false,
refresh = {
statusline = 1000,
tabline = 1000,
winbar = 1000,
},
},
},
},
-- Completion
{
"hrsh7th/nvim-cmp",
event = "InsertEnter", -- Load only when entering insert mode
dependencies = {
"hrsh7th/cmp-nvim-lsp",
"hrsh7th/cmp-buffer",
"hrsh7th/cmp-path",
"hrsh7th/cmp-cmdline",
"L3MON4D3/LuaSnip",
"saadparwaiz1/cmp_luasnip",
},
opts = function()
local cmp = require("cmp")
local luasnip = require("luasnip")
return {
snippet = {
expand = function(args)
luasnip.lsp_expand(args.body)
end,
},
window = {
completion = cmp.config.window.bordered(),
documentation = cmp.config.window.bordered(),
},
mapping = cmp.mapping.preset.insert({
["<C-b>"] = cmp.mapping.scroll_docs(-4),
["<C-f>"] = cmp.mapping.scroll_docs(4),
["<C-Space>"] = cmp.mapping.complete(),
["<C-e>"] = cmp.mapping.abort(),
["<CR>"] = cmp.mapping.confirm({ select = true }), -- Accept currently selected item. Set `select` to `false` to only confirm explicitly selected items.
["<Tab>"] = cmp.mapping(function(fallback)
if cmp.visible() then
cmp.select_next_item()
elseif luasnip.expand_or_jumpable() then
luasnip.expand_or_jump()
else
fallback()
end
end, { "i", "s" }),
["<S-Tab>"] = cmp.mapping(function(fallback)
if cmp.visible() then
cmp.select_prev_item()
elseif luasnip.jumpable(-1) then
luasnip.jump(-1)
else
fallback()
end
end, { "i", "s" }),
}),
sources = cmp.config.sources({
{ name = "nvim_lsp" },
{ name = "luasnip" },
{ name = "buffer" },
{ name = "path" },
}),
}
end,
},
-- LSP
{
"neovim/nvim-lspconfig",
event = "BufReadPre", -- Load before reading buffer
dependencies = {
"williamboman/mason.nvim",
"williamboman/mason-lspconfig.nvim",
"hrsh7th/nvim-cmp",
"hrsh7th/cmp-nvim-lsp",
},
opts = {
-- Use a protected call so we don't error out if mason is not installed
-- We'll handle the error when we load the plugins
capabilities = require("capabilities"),
},
},
-- Git
{
"tpope/vim-fugitive",
cmd = "G", -- Load only when G command is called
},
-- Formatting
{
"jose-elias-alvarez/null-ls.nvim",
event = "BufReadPre",
dependencies = { "nvim-lspconfig" },
opts = {
sources = {
require("null-ls").builtins.formatting.prettier,
require("null-ls").builtins.formatting.black,
require("null-ls").builtins.diagnostics.eslint,
},
},
},
}
The core problem lazy.nvim solves is the startup time penalty associated with loading every single plugin when Neovim launches. Instead of eagerly loading everything, lazy.nvim uses a declarative approach, allowing you to specify when and how plugins should be loaded. This means your Neovim instance can be lean and mean on startup, only bringing in the functionality you actually need at that moment.
Here’s how it works under the hood:
-
Declarative Configuration: You define your plugins in a Lua table. Each plugin is a table with keys like
dir(the plugin’s directory),url(its Git repository),dependencies, and crucially,event,cmd,ft,keys, orconfig. These keys telllazy.nvimwhen to trigger the plugin’s loading. -
Lazy Loading Triggers:
event: Loads the plugin when a specific Neovim event fires (e.g.,BufReadPostfor opening files,InsertEnterfor entering insert mode,VimEnterfor Neovim startup).cmd: Loads the plugin only when a specific Ex command is invoked (e.g.,Telescopefor fuzzy finding,Gfor Git operations).ft: Loads the plugin when a buffer of a specific filetype is opened (e.g.,lua,python).keys: Loads the plugin when a specific keybinding is pressed.config: Loads the plugin when its configuration function is called.
-
Dynamic Loading: When one of these triggers occurs,
lazy.nvimintercepts the action, loads the specified plugin, executes its configuration, and then allows the original action to proceed. This happens so quickly that it’s often imperceptible to the user. -
Dependency Management:
lazy.nvimautomatically handles plugin dependencies. If plugin A depends on plugin B, and plugin A is triggered for loading,lazy.nvimwill ensure plugin B is loaded first. -
Plugin Management: It also handles installing, updating, and cleaning up plugins, making the entire lifecycle seamless.
The most surprising thing about lazy.nvim is how its event-driven loading model directly maps to user intent. You don’t think about "loading" plugins; you think about using features. When you hit <C-p> to search for a file, Telescope loads. When you enter insert mode, nvim-cmp loads. This separation of concerns is incredibly powerful for optimizing performance without sacrificing functionality.
Let’s break down the configuration from the example:
nvim-treesitter:event = { "BufReadPost", "BufNewFile" }. This means Treesitter’s syntax highlighting and indentation will be ready as soon as you open or create a file, but not before.telescope.nvim:cmd = "Telescope". Telescope is a prime example of a plugin that shouldn’t load on startup. It only loads when you actually type:Telescopeor map a key to it, saving precious milliseconds.nvim-lualine.nvim:event = "VimEnter". The status line is a core UI element, so it’s loaded once when Neovim starts.nvim-cmp:event = "InsertEnter". Autocompletion is only relevant when you’re typing, so it’s deferred until you enter insert mode.nvim-lspconfig:event = "BufReadPre". The LSP client needs to be available before files are read to enable features like code navigation and diagnostics on load.vim-fugitive:cmd = "G". Like Telescope, Git commands are used selectively, sovim-fugitivewaits until you use:G.null-ls.nvim:event = "BufReadPre". Formatting and linting tools are typically needed when files are being processed or saved, so loading them early ensures they are ready.
The configuration for opts allows you to pass specific settings to plugins. For nvim-cmp, the opts is a function that returns a table of configuration. This allows for dynamic configuration based on other plugins or context, which is a common pattern.
The build = ":TSUpdate" for nvim-treesitter is a special instruction for lazy.nvim to run a command after the plugin is installed or updated, ensuring its native dependencies are built.
A common pitfall is over-eager loading. For instance, if you set event = "VimEnter" for every plugin, you’re back to square one with slow startup times. The art of lazy.nvim is in correctly identifying the minimal trigger for each plugin. Sometimes, a plugin might have internal logic that triggers its own loading, and you can rely on that by omitting explicit triggers if the plugin handles it well.
The next concept you’ll likely explore is customizing plugin installation sources beyond just Git URLs, such as local paths or even plugins managed by other package managers.