Migrating from Packer to Lazy.nvim: A 15x Faster Startup and Why Packer is Dead
November 06, 2025
In the ever-evolving world of Neovim plugins, staying current isn’t just about features—it’s about performance. My recent migration from Packer to Lazy.nvim didn’t just modernize my setup; it transformed it, delivering a staggering 15x improvement in startup time. Let’s unpack this migration, explore the nitty-gritty changes, and discuss why Packer, once a favorite, has become a relic of the past.
The Packer Exodus: Why I Finally Made the Switch
For years, Packer served me well as my Neovim plugin manager. It was the go-to choice when I first embraced Lua-based configurations. But as the ecosystem matured, cracks began to appear:
- Stagnation: The repository has seen minimal activity, with pull requests languishing unmerged for months.
- Maintenance Burden: The maintainer has shifted focus, leaving users to fend for themselves with bugs and compatibility issues.
- Performance Bottlenecks: While functional, Packer lacks the optimizations that modern managers like Lazy offer.
Lazy.nvim, created by the same team behind some of Neovim’s most popular plugins, emerged as the natural successor. It’s not just faster—it’s designed for the way we use Neovim today, with lazy-loading and performance-first architecture.
Diving into the Migration: PR #8 in Detail
This PR migrates the Neovim configuration from Packer to lazy.nvim, replacing the plugin manager while adding performance optimizations like lazy loading. The main file changed is init.lua (+42 additions, -98 deletions). Other files include init.lua.back (backup), lazy-lock.json (new lockfile), and nvim.png (likely an icon update).
Core Changes: From Packer to Lazy
The heart of the migration was replacing Packer’s startup function with Lazy’s declarative setup. Instead of:
packer.startup(function(use)
use 'wbthomason/packer.nvim'
use 'junegunn/fzf'
-- ... (all plugin 'use' calls)
end)
Lazy uses a table-based approach with bootstrap code:
-- Bootstrap lazy.nvim
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",
"https://github.com/folke/lazy.nvim.git",
"--branch=stable",
lazypath,
})
end
vim.opt.rtp:prepend(lazypath)
-- Setup lazy.nvim
require("lazy").setup({
-- plugin specs here
})
Lazy-Loading: The Performance Game-Changer
Migrated plugins with Lazy’s sophisticated lazy-loading system, adding event, cmd, keys, build, and dependencies for performance.
Example before (Packer):
use {'neoclide/coc.nvim', branch = 'master', run = 'npm ci'}
use {'tpope/vim-commentary'}
use {'nvim-lualine/lualine.nvim', requires = { 'nvim-tree/nvim-web-devicons', opt = true }}
After (lazy):
{ 'neoclide/coc.nvim', branch = 'master', build = 'npm ci', event = 'BufReadPre' },
{ 'tpope/vim-commentary', keys = { { 'gc', mode = { 'n', 'v' } } } },
{
'nvim-lualine/lualine.nvim',
dependencies = { 'nvim-tree/nvim-web-devicons' },
event = 'VeryLazy'
}
This implements multiple loading strategies:
- Event-based loading: Plugins like
fzf.vimnow load only when the:Filescommand is invoked - Keymap triggers:
nvim-treeloads when pressing<C-n> - Command loading: Plugins like Copilot activate only when needed
Lualine Optimization: Cutting the Fat
Updated lualine config to enable global statusline and remove expensive refresh events:
require('lualine').setup {
-- ...
globalstatus = true, -- Changed from false
refresh = {
-- Removed 'CursorMoved' and 'CursorMovedI' events
statusline = 1000,
tabline = 1000,
winbar = 1000,
refresh_time = 16, -- ~60fps
events = {
'WinEnter',
'BufEnter',
'BufWritePost',
'SessionLoadPost',
'FileChangedShellPost',
'VimResized',
'Filetype',
'ModeChanged',
},
},
}
Deferred Setup: Non-Critical Plugins on Ice
Moved non-critical setups into vim.defer_fn() for startup performance:
-- Defer non-critical setup
vim.defer_fn(function()
require("ibl").setup()
require("nvim-tree").setup()
end, 0)
This prevents plugins like indent-blankline (ibl) and nvim-tree from blocking the main UI thread during startup.
Code Cleanup: Removing Duplicates
Eliminated duplicate autocmd definitions and redundant code, such as multiple yaml filetype sets and a typescript callback that auto-saved files. Removed about 60 lines without losing functionality.
The Numbers Don’t Lie: Performance Gains
The results speak for themselves:
- Before: 1797ms startup time
- After: 115ms startup time
- Improvement: ~15x faster
That’s the difference between waiting almost 2 seconds and having Neovim ready in under a tenth of a second. In practical terms, this means instant responsiveness when opening files, switching projects, or just launching the editor.
Why Packer is Dead
Packer is showing its age and is barely maintained, with stagnant development and unresolved issues. For peace of mind, moving to lazy.nvim is the best option.
Migration Tips for Fellow Neovim Users
If you’re considering this migration:
- Start small: Migrate one plugin at a time to understand Lazy’s syntax
- Leverage lazy-loading: Not just for performance, but for cleaner keymap management
- Profile first: Use
:Lazy profileto identify your biggest performance bottlenecks - Preserve your workflow: Lazy can replicate any Packer functionality with better performance
Conclusion: The Future is Lazy
This migration wasn’t just about faster startups—it was about embracing the future of Neovim plugin management. Lazy.nvim represents the next evolution, combining performance, maintainability, and modern features in a package that’s actively developed and community-supported.
If your Neovim feels sluggish, it’s worth the switch. The 15x improvement in my case might be the extreme end, but even conservative estimates show 3-5x improvements for most users. In the world of text editors, every millisecond counts, and Lazy delivers them in spades.
The code for this migration lives in my nvim config repo, and I’m happy to help if you’re undertaking a similar journey. What’s holding you back from making the switch?