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?