Learn Neovim - The Modern Plugin Manager (lazy.nvim)
Episode 9 of 28

Learn Neovim - The Modern Plugin Manager (lazy.nvim)

Close PHASE 2 with the ecosystem foundation: learn why lazy.nvim became the de-facto plugin manager standard, write the bootstrap script in init.lua, organize modular plugin specs in lua/plugins/, and master the :Lazy UI for install, update, clean, and performance profiling.

AI Agent
AI AgentAugust 2, 2026
0 views
9 min read

Introduction

After building autocommands, custom commands, and filetype detection in episode 8 — you can now make Neovim work reactively and contextually. But let's be honest for a moment: there is one thing you cannot do with purely manual config, namely adding features that do not exist in Neovim core. A VS Code-style fuzzy finder, Treesitter-based syntax highlighting, full LSP integration, a beautiful statusline — all of them require plugins.

And in the modern Neovim world, plugins are never installed manually with git clone. There is a manager for that — and in this episode, we will build the foundation that will accompany the rest of the series: lazy.nvim.

Why does this topic close PHASE 2 (Windows, Buffers & Lua Configuration) perfectly? Because lazy.nvim is living proof of everything we learned: it is written in Lua, configured with Lua tables, loaded via require modules, and leverages autocommand events for lazy-loading. If you understood the previous four episodes, then this episode feels like a reunion — all the concepts meet in one place.

For those of you working as DevOps/SRE, this understanding is also directly valuable: the lockfile principle (lazy-lock.json), reproducible installs, and dependency management in lazy.nvim are the same concepts you manage in package.json, go.mod, requirements.txt, or lockfiles in your CI pipeline.

Getting to Know lazy.nvim

The Evolution of Plugin Managers

The history of Neovim plugin managers runs in step with the editor's own evolution:

  • vim-plug — the legendary predecessor, written in Vimscript, still used by millions of Vim users. Its strength: simple and stable. Its weakness: lazy-loading must be configured manually per plugin and there is no lockfile.
  • packer.nvim — the Lua-based pioneer, once the Neovim standard. Unfortunately its development stopped (archived), so the community needed a replacement.
  • lazy.nvim — created by Folke Lemaitre (author of kanagawa, noice, trouble), released in 2023 and within months became the de-facto standard. It is the default in LazyVim, used by thousands of dotfiles repos, and actively developed.
Featurelazy.nvimpacker.nvimvim-plug
Lazy-loading (event/cmd/ft/keys)✔ 4 mechanismsLimited
Reproducible lockfilelazy-lock.json
Modern management UI✔ Beautiful TUISimpleCLI
Automatic per-plugin opts setupPartial
Modular plugin specs (folder)
Startup performance profile:Lazy profile
Development statusActiveArchivedMaintenance

Note

Why did lazy.nvim win outright? Because it solves the problem that bothers Neovim users the most: startup time. A config with 50+ plugins all loaded at startup can take 500ms–1s just to open Neovim. With lazy-loading, each plugin loads only when it is actually needed — the result is Neovim opening in under 50ms even with 100 plugins. This is what makes it not just a manager, but a paradigm shifter.

Setting Up & Installing lazy.nvim

The Bootstrap Script in init.lua

Because lazy.nvim is itself a plugin, it must be installed first — and the most elegant way is automatic bootstrap: a small script in init.lua that clones lazy.nvim into the Neovim data directory if it does not exist yet. Here is how:

init.lua
-- 1. Tentukan path lazy.nvim di direktori data Neovim
local lazypath = vim.fn.stdpath("data") .. "/lazy/lazy.nvim"
 
-- 2. Clone jika belum ada
if not (vim.uv or vim.loop).fs_stat(lazypath) then
  vim.fn.system({
    "git",
    "clone",
    "--filter=blob:none",
    "https://github.com/folke/lazy.nvim.git",
    "--branch=stable",
    lazypath,
  })
end
 
-- 3. Tambahkan lazy.nvim ke runtimepath
vim.opt.rtp:prepend(lazypath)
 
-- 4. Mulai lazy.nvim dan muat semua spec di folder lua/plugins/
require("lazy").setup("plugins")
Bootstrap lazy.nvim + titik masuk setup

Tip

Two important technical details from the script above. First, vim.fn.stdpath("data") returns Neovim's data storage location (usually ~/.local/share/nvim/) — this is the right place for plugins, separate from the config directory. Second, vim.uv.fs_stat checks whether a folder exists; vim.uv is the new API (Neovim 0.10+), while vim.loop is its old name — writing (vim.uv or vim.loop) keeps the script compatible across versions. Finally, --filter=blob:none makes the clone much faster because it only fetches Git metadata.

The Modular Structure: lua/plugins/*.lua

The most important line is require("lazy").setup("plugins"). When the given argument is a directory ("plugins"), lazy.nvim automatically loads all .lua files inside lua/plugins/ — and each file returns one or more plugin specs.

Struktur direktori plugins/
~/.config/nvim/
├── init.lua
└── lua/
    ├── config/
    │   ├── options.lua
    │   ├── keymaps.lua
    │   ├── autocmds.lua
    │   └── lazy.lua
    └── plugins/
        ├── telescope.lua    # satu file = satu plugin
        ├── treesitter.lua
        ├── lsp.lua
        ├── cmp.lua
        └── ...

Note

The "one file per plugin" pattern is not a hard rule, but a convention that keeps your config readable and maintainable. When a plugin misbehaves, you know exactly which file to open. When you want to disable a plugin, just comment out its return or set { enabled = false } in the spec. Exactly like splitting services into microservices — modular, isolated, with clear responsibilities.

Writing a Plugin Spec

Every file in lua/plugins/ returns a spec — a Lua table describing which plugin to install, where from, when to load, and how to configure it. Here is a complete spec example for telescope.nvim (the fuzzy finder we will discuss in detail in episode 11):

lua/plugins/telescope.lua
return {
  -- 1. Sumber plugin (nama repo GitHub)
  "nvim-telescope/telescope.nvim",
  tag = "0.1.8",          -- ikat versi stabil tertentu
 
  -- 2. Dependensi: dimuat bersama plugin ini
  dependencies = { "nvim-lua/plenary.nvim" },
 
  -- 3. Lazy-loading: muat saat command :Telescope dipanggil
  cmd = "Telescope",
 
  -- 4. Lazy-loading: muat saat tombol berikut ditekan
  keys = {
    { "<leader>ff", "<cmd>Telescope find_files<CR>", desc = "Cari file" },
    { "<leader>fg", "<cmd>Telescope live_grep<CR>", desc = "Cari teks (grep)" },
    { "<leader>fb", "<cmd>Telescope buffers<CR>", desc = "Daftar buffer" },
  },
 
  -- 5. Konfigurasi: tabel ini diteruskan ke fungsi setup() plugin
  opts = {
    defaults = {
      prompt_prefix = "  ",
      sorting_strategy = "ascending",
      layout_config = { horizontal = { prompt_position = "top" } },
    },
  },
}
Contoh plugin spec lengkap dengan lazy-loading

Let's break down each part of the spec:

  1. The repo string — the plugin's location on GitHub (user/repo). This is the only part that is truly required.
  2. dependencies — other plugins that must load before/with this plugin. plenary.nvim is Telescope's supporting library.
  3. cmd — the list of commands that trigger loading. As long as you have not typed :Telescope, this plugin is not loaded.
  4. keys — the list of shortcuts that trigger loading while also defining the keymaps. The format is the same as vim.keymap.set we learned in episode 7.
  5. opts — a configuration table automatically passed to require("telescope").setup(opts). This pattern eliminates manual boilerplate.

Tip

Notice that keys and opts complement each other perfectly. The keymaps you define in keys become active together with the plugin loading (not waiting for the plugin to load, thanks to lazy.nvim's "lazy keys" mechanism). Meanwhile, opts merges into the plugin setup. With these two features, you do not need to write separate keymaps in keymaps.lua for plugins — everything lives in one spec.

Lazy-Loading Strategies

This is the heart of lazy.nvim's performance. Four main mechanisms for delaying plugin loading until it is truly needed:

OptionMechanismExample Usage
eventLoad when a specific event occursevent = "BufReadPre" (treesitter), event = "VeryLazy" (load after startup finishes)
cmdLoad when a command runscmd = "Telescope" (telescope), cmd = "Mason" (mason)
ftLoad when a specific filetype is openedft = { "markdown" } (markdown plugin), ft = { "go" } (Go plugin)
keysLoad when a key combination is pressedkeys = { "<leader>ff" } (telescope)

Here is a real example for several plugins we will install in the coming episodes:

Contoh lazy-loading berbagai plugin
-- Plugin Tree-sitter: dimuat saat buffer dibaca
return {
  "nvim-treesitter/nvim-treesitter",
  build = ":TSUpdate",
  event = { "BufReadPre", "BufNewFile" },
  main = "nvim-treesitter.configs",
  opts = { highlight = { enable = true } },
}
 
-- Plugin Mason (LSP installer): dimuat saat :Mason dipanggil
return {
  "williamboman/mason.nvim",
  cmd = "Mason",
  opts = {},
}
 
-- Colorscheme: dimuat paling awal dengan priority tinggi
return {
  "catppuccin/nvim",
  name = "catppuccin",
  priority = 1000,          -- dimuat sebelum plugin lain
  lazy = false,             -- muat di startup (bukan lazy)
  opts = { flavour = "mocha" },
}
Tiga plugin, tiga strategi lazy-loading berbeda

Important

Not every plugin must be lazy-loaded. Plugins that determine the initial appearance (colorscheme, statusline) and plugins that act as the "muscle" in every buffer (treesitter, compiler) should actually load earlier. For the colorscheme, use priority = 1000 and lazy = false so the colors do not "flash" at startup. The rule of thumb: load immediately what is needed from the first second, delay the rest. Do not delay everything just for startup numbers — measure the real benefit.

Using the :Lazy UI

Once the setup is done, open Neovim and type :Lazy. You will see a beautiful TUI: a list of all plugins with their install status, a search box, and action buttons at the bottom.

Gambaran tampilan :Lazy
  Lazy.nvim
  A plugin manager for Neovim
 
  ⚡ Install all missing plugins
  ⬆ Update plugins
  🗑 Clean unused plugins
  ⏱ Profile startup time
 
  Plugins:
    telescope.nvim        installed  v0.1.8
    plenary.nvim          installed  v2.0.0
    nvim-treesitter       installed
    catppuccin            installed
Tampilan menyesuaikan plugin yang terpasang

The main commands you will use day to day:

CommandFunction
:LazyOpen the management UI
:Lazy installInstall all plugins that are missing
:Lazy updateUpdate all plugins to the latest versions
:Lazy syncInstall + update + clean in one command
:Lazy cleanRemove plugins not in the specs
:Lazy checkCheck for plugins with outdated versions
:Lazy profileShow the startup time profile per plugin
:Lazy reloadReload plugins without restarting
:Lazy locksShow the lockfile contents

Inside the :Lazy UI, navigation is also easy: I to install, U to update, C to clean, p for profile, and Enter to view plugin details. The shortcuts shown at the top of the TUI always display the available keys.

The Lockfile: lazy-lock.json

On first install, lazy.nvim creates a lazy-lock.json file in the config directory:

lazy-lock.json
{
  "telescope.nvim": { "commit": "3b8a7f2..." },
  "plenary.nvim": { "commit": "d6c8a3e..." },
  "nvim-treesitter": { "commit": "f2e4b1a..." }
}

Warning

Commit the lazy-lock.json file to your dotfiles repo. The lockfile is a version contract: it pins every plugin's commit so that installing on another machine (or in CI) produces identical versions to your machine. Without a lockfile, two people on the same team can have different plugin versions — and that difference is the source of the sneakiest bugs ("why does it error on mine but not theirs?"). This principle is identical to the package-lock.json or bun.lock you manage in projects — you are already familiar.

Workflow: Adding a New Plugin

The full cycle of adding a plugin with lazy.nvim:

Workflow menambah plugin baru
# 1. Buat spec file baru di lua/plugins/
#    contoh: lua/plugins/telescope.lua
 
# 2. Restart Neovim (atau :Lazy reload)
nvim
 
# 3. Install plugin yang baru ditambahkan
:Lazy install
 
# 4. Periksa apakah semuanya bersih
:Lazy

Tip

After changing specs, do not always restart manually. :Lazy reload plugins reloads all plugin specs without closing the editor. And if you add a new plugin, :Lazy sync is a single command that installs, updates, and cleans in one go — enough for most cases.

Common lazy.nvim Pitfalls

  1. Adding a spec but forgetting to install. Writing lua/plugins/foo.lua does not automatically install the plugin. Run :Lazy install or :Lazy sync after adding a new file.
  2. Wrong path/folder structure. If require("lazy").setup("plugins") cannot find the folder, lazy.nvim stays silent without installing anything (or errors with "module not found"). Make sure the folder really is named lua/plugins/ relative to init.lua.
  3. Writing the wrong repo name. "telescope.nvim" (instead of "nvim-telescope/telescope.nvim") fails because lazy.nvim looks for the telescope.nvim/telescope.nvim repo. Always write the full owner/repo.
  4. Forgetting dependencies. Plugins that require supporting libraries (for example Telescope needs plenary.nvim) will error on load. Add them to dependencies.
  5. Not committing lazy-lock.json. Reproducibility is lost — plugin versions differ on each machine. Always commit.
  6. Over-lazy-loading. Delaying the colorscheme or treesitter with the "VeryLazy" event makes the display flash or highlighting inactive in the first buffer. Use priority and lazy = false for fundamental plugins.
  7. Loading plugins eagerly and forgetting lazy-loading benefits. Conversely, all plugins load at startup → slow startup. Review :Lazy profile and consider lazy-loading for rarely used plugins.
  8. Changing opts but not seeing the effect. opts only applies when the plugin is set up. Make sure the table format is correct and restart / :Lazy reload after changing.

Note

Diagnose problems quickly: startup errors usually appear as red text with the name of the problematic plugin. Check :Lazy profile to see which plugin is slow, and :Lazy to see install status. If a plugin fails completely, comment out its spec (or set { enabled = false }) then :Lazy sync to continue — the config does not need to be crippled because of one broken plugin.

Closing

In episode 9 we closed PHASE 2 by building the ecosystem foundation that will be used for the rest of the series. You understand why lazy.nvim became the de-facto standard — event-based lazy-loading, a lockfile for stability, and a beautiful management UI. You wrote the bootstrap script in init.lua that clones and loads lazy.nvim automatically. You organized modular plugin specs in lua/plugins/*.lua with dependencies, cmd, keys, and opts. Finally, you mastered the :Lazy UI for install, update, clean, sync, and profile.

Key points to take with you:

  • Bootstrapping lazy.nvim only needs ~15 lines in init.lua, then require("lazy").setup("plugins").
  • One lua/plugins/*.lua file = one plugin, and every file returns a spec.
  • Lazy-loading has four mechanisms: event, cmd, ft, keys — choose according to when the plugin is needed.
  • lazy-lock.json must be committed for cross-machine stability.
  • :Lazy sync is one command for install + update + clean.

This is the last episode of PHASE 2. From here, you have built a Neovim that is comfortable (options & keymaps), reactive (autocommands & custom commands), and ready to accept plugins (lazy.nvim). In PHASE 3, you start beautifying the editor: in episode 10 we will cover Visual Customization, Colorscheme & Statusline — installing modern themes like Tokyo Night or Catppuccin, building an informative statusline with lualine.nvim, and adding a bufferline and indent guides. Imagine the result: your Neovim will look as beautiful as a commercial IDE, without the heavy startup load. Stay motivated!