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:

  1. 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, or config. These keys tell lazy.nvim when to trigger the plugin’s loading.

  2. Lazy Loading Triggers:

    • event: Loads the plugin when a specific Neovim event fires (e.g., BufReadPost for opening files, InsertEnter for entering insert mode, VimEnter for Neovim startup).
    • cmd: Loads the plugin only when a specific Ex command is invoked (e.g., Telescope for fuzzy finding, G for 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.
  3. Dynamic Loading: When one of these triggers occurs, lazy.nvim intercepts 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.

  4. Dependency Management: lazy.nvim automatically handles plugin dependencies. If plugin A depends on plugin B, and plugin A is triggered for loading, lazy.nvim will ensure plugin B is loaded first.

  5. 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 :Telescope or 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, so vim-fugitive waits 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.

Want structured learning?

Take the full Vim course →