If your current Neovim setup still uses packer.nvim, this guide will help you move to lazy.nvim without guessing. It explains what changes, what stays the same, how to rewrite your plugin specs, and how to structure the new setup in a beginner-friendly way.
packer.nvim is unmaintained, and its own README recommends lazy.nvim.
No manual compile step, cleaner plugin specs, automatic module lazy-loading, and a lockfile workflow.
You are not rebuilding Neovim from scratch. You are replacing the plugin manager and translating the plugin definitions.
What this article assumes
This guide assumes you already have a working Neovim configuration and that your plugins are currently declared with packer.nvim. You do not need expert Lua knowledge. You only need to be comfortable editing your Neovim config files and reading simple plugin declarations.
References: packer.nvim repo · packer.nvim README · lazy.nvim repo · lazy.nvim migration guide
The important context is simple. packer.nvim was a very influential plugin manager, but its README now says the repository is unmaintained and recommends lazy.nvim. That means a migration is no longer just about taste. For many users, it is the most practical maintenance move.
lazy.nvim. LazyVim is a full Neovim configuration built on top of lazy.nvim. In this post, we use LazyVim mainly as a model for file structure, not as the thing you are migrating to.
What actually changes when you leave packer.nvim
References: packer.nvim README · lazy.nvim README · lazy.nvim plugin spec · lazy.nvim usage
With packer.nvim, your plugins usually live inside a require("packer").startup(function(use) ... end) block. Packer also expects a compile step to generate its lazy-loader file. In many configs, that leads to a PackerCompile autocommand and a generated packer_compiled.lua or packer_compiled.vim file.
With lazy.nvim, the plugin manager works with plain plugin spec tables. You do not wrap everything in use(...), and you do not manually compile a loader file. Instead, lazy.nvim manages loading, caching, and startup behavior itself.
- Packer: startup block,
usedeclarations, manual compile workflow. - Lazy: plain spec tables, automatic loading rules, no manual compile command.
- Packer: commands like
:PackerSyncand:PackerCompile. - Lazy: commands like
:Lazy sync,:Lazy update,:Lazy clean,:Lazy profile, and alazy-lock.jsonfile for reproducible plugin revisions.
Use a file layout that matches lazy.nvim and LazyVim
References: lazy.nvim installation · lazy.nvim structuring docs · LazyVim configuration · LazyVim plugins
For a beginner migration, the cleanest structure is the one lazy.nvim already documents and LazyVim also uses: put the bootstrap and setup logic in lua/config/lazy.lua, then put plugin specs in one or more files under lua/plugins/.
A minimal target layout looks like this:
~/.config/nvim
├── init.lua
└── lua
├── config
│ └── lazy.lua
└── plugins
├── core.lua
├── lsp.lua
└── ui.lua
Your init.lua can stay very small:
require("config.lazy")
Then create a simple lua/config/lazy.lua bootstrap file:
local lazypath = vim.fn.stdpath("data") .. "/lazy/lazy.nvim"
if not vim.loop.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)
vim.g.mapleader = " "
vim.g.maplocalleader = "\\"
require("lazy").setup({
{ import = "plugins" },
}, {
checker = { enabled = true },
})
That structure gives you a clean migration target. You can start with one lua/plugins/init.lua file if you want, then split plugins into smaller files later.
The safest step-by-step migration path
- Back up your current config. Copy your entire Neovim config directory somewhere safe before you touch anything.
- List the plugins you already use. Open your current
packer.nvimfile and identify the actual plugin declarations you want to keep. - Remove packer-specific bootstrapping. Delete the
ensure_packer()block and removeuse "wbthomason/packer.nvim"from your managed plugins. - Remove packer-specific compile behavior. Delete any
BufWritePost ... | PackerCompileautocommands and stop loading any generatedpacker_compiledfile. - Add the lazy.nvim bootstrap file. Create
init.luaandlua/config/lazy.luaas shown above. - Move plugin specs into
lua/plugins/. Start with one file if that feels easier. - Translate each packer spec into a lazy spec. This is the main work. The field names are similar, but not identical.
- Start Neovim and run
:Lazy sync. This installs missing plugins, updates the set, and cleans what is no longer needed. - Run
:Lazy healthand:Lazy profile. Confirm that lazy.nvim itself is healthy and inspect startup behavior once the migration works.
PackerCompile on save, you will keep one foot in the old system and make debugging harder.
How packer fields map to lazy fields
Reference: lazy.nvim migration guide
setupbecomesinitrequiresbecomesdependenciesasbecomesnameopt = truebecomeslazy = truerunbecomesbuildlockbecomespindisable = truebecomesenabled = falsetag = "*"becomesversion = "*"afteris usually unnecessary; usedependenciesonly when a plugin must be installed and loaded with another pluginconfigcan still exist, but string-based config does not; use a function insteadmoduleusually does not need to be declared because lazy.nvim auto-loads Lua modules on demand
dependencies should not become your new default hammer. Many Lua plugins can simply be installed normally and loaded when they are require()d. Only group dependencies when they truly belong to the same plugin load path.
How to rewrite common plugin specs
1) Simple plugin
Packer
use "nvim-lualine/lualine.nvim"Lazy
return {
{ "nvim-lualine/lualine.nvim", opts = {} },
}
In lazy.nvim, opts = {} is usually the cleanest way to say, “call this plugin’s setup() with these options.” The docs recommend using opts instead of config whenever possible.
2) Dependency-based plugin
Packer
use {
"lewis6991/gitsigns.nvim",
requires = { "nvim-lua/plenary.nvim" },
config = function()
require("gitsigns").setup()
end,
}Lazy
return {
{
"lewis6991/gitsigns.nvim",
dependencies = { "nvim-lua/plenary.nvim" },
opts = {},
},
}3) Command or event lazy-loading
Packer
use {
"iamcco/markdown-preview.nvim",
run = "cd app && yarn install",
cmd = "MarkdownPreview",
}Lazy
return {
{
"iamcco/markdown-preview.nvim",
build = "cd app && yarn install",
cmd = "MarkdownPreview",
},
}4) Filetype loading
Packer
use {
"andymass/vim-matchup",
event = "VimEnter",
}Lazy
return {
{
"andymass/vim-matchup",
event = "VimEnter",
},
}5) Colorscheme plugin
Colorschemes are the main place where you often do not want lazy loading in the same way you do for ordinary plugins. lazy.nvim’s docs recommend giving your main colorscheme high priority when it loads as a start plugin.
return {
{
"folke/tokyonight.nvim",
lazy = false,
priority = 1000,
},
}What to use instead of packer commands
References: packer.nvim README · lazy.nvim usage · lazy.nvim lockfile
:PackerSyncbecomes:Lazy sync:PackerUpdatebecomes:Lazy update:PackerCleanbecomes:Lazy clean:PackerStatusbecomes:Lazy:PackerCompilebecomes nothing, because lazy.nvim does not require a manual compile step- Packer snapshots become the
lazy-lock.jsonworkflow plus:Lazy restore PackerProfilebecomes:Lazy profile
Once the migration is complete, commit your lazy-lock.json file. That gives you reproducible plugin revisions across machines and a clean restore target later.
What usually breaks a beginner migration
- Leaving old packer code in place. If your config still sources or compiles packer files, you are not fully migrated.
- Trying to keep the
use(...)mental model. In lazy.nvim, think in spec tables, not wrapped declarations. - Translating
setuptoconfiginstead ofinit. That changes when the code runs. - Using
configeverywhere. Preferoptswhen a plugin exposessetup(). - Overusing
dependencies. lazy.nvim can auto-load Lua modules when they are required, so not every helper library needs to be nested. - Turning on global lazy-loading too early. lazy.nvim supports
defaults.lazy = true, but the docs explicitly warn that you should only do that if you understand the consequences. - Forgetting health and profiling checks. A migration is not done just because Neovim opens. Run
:Lazy healthand:Lazy profile.
What LazyVim teaches even if you do not use LazyVim
References: LazyVim repo · LazyVim README · LazyVim configuration docs · LazyVim plugin docs
LazyVim is useful to study because it proves that a large Neovim setup can stay clean when it is organized around lazy.nvim conventions. Its docs show a small init.lua, a dedicated lua/config/lazy.lua file, and plugin specs living under lua/plugins/. That is a strong model for a beginner migration because it gives you a place for each kind of code.
It also reinforces an important point: plugin configuration in LazyVim is still just lazy.nvim-style plugin specs. So even if you never install LazyVim itself, its file layout is a very good template for cleaning up a formerly packer-based config.
Frequently Asked Questions
These are the questions most beginners have when they first replace packer.nvim with lazy.nvim.
Do I need to rewrite my whole Neovim config?
No. You mainly need to replace the plugin manager bootstrap, move to lazy.nvim’s spec format, and remove packer-specific compile behavior.
Do I need to switch to LazyVim too?
No. LazyVim is a full Neovim setup built on lazy.nvim. You can migrate to lazy.nvim without adopting LazyVim.
What happens to packer_compiled.lua?
You no longer need it. Remove the old generated file and any autocommands that regenerate it.
What should I do with use "wbthomason/packer.nvim"?
Delete it. That line was packer managing itself. It does not belong in a lazy.nvim plugin list.
Should every plugin become lazy-loaded?
No. Some plugins should load immediately, especially your colorscheme and anything needed very early in startup. The goal is better loading choices, not maximum laziness.
What replaces packer snapshots?
The normal lazy.nvim workflow is to commit lazy-lock.json and use :Lazy restore when you want the exact locked plugin versions again.
What is the cleanest first migration target?
A tiny init.lua, a single lua/config/lazy.lua bootstrap file, and one plugin file under lua/plugins/. Once that works, split your plugins into multiple files.
How do I know the migration is really done?
Neovim should start with lazy.nvim, :Lazy sync should manage your plugins, there should be no packer compile autocommand left, and your plugin revisions should be tracked in lazy-lock.json.
Conclusion
A good migration from packer.nvim to lazy.nvim is less dramatic than it first appears. The real work is not “learning a whole new Neovim.” The real work is changing the plugin manager, translating the plugin declarations, and adopting a cleaner structure.
If you follow the order in this article, you can make that move without losing your setup. First remove packer-specific bootstrapping and compile behavior. Then install lazy.nvim with a small structured layout. Then translate one plugin spec at a time. Finally, use :Lazy sync, :Lazy health, :Lazy profile, and lazy-lock.json to verify that the new system is working the way you expect.
That is the real goal. Not just “switch plugin managers,” but end up with a Neovim config that is easier to maintain, easier to reason about, and better aligned with the current plugin ecosystem.
Comments
Post a Comment