Performance Optimization Reference
Complete guide for optimizing Neovim startup and runtime performance.
Target Metrics
| Metric | Target | Measure With |
|---|---|---|
| Startup Time | <50ms | :Lazy profile |
| Plugin Count | 82 | :Lazy |
| Lazy Loaded | ~95% | :Lazy |
| Memory (startup) | <50MB | :lua print(collectgarbage("count")) |
Startup Optimization Layers
Layer 1: Module Caching (~50ms savings)
-- init.lua (first line)
vim.loader.enable()
This caches Lua bytecode for faster subsequent loads.
Layer 2: Skip vim._defaults (~180ms savings)
-- init.lua
vim.g.skip_defaults = true -- Skip default vim settings
Layer 3: Disable Providers (~10ms savings)
-- lua/config/performance.lua
vim.g.loaded_node_provider = 0
vim.g.loaded_perl_provider = 0
vim.g.loaded_ruby_provider = 0
Layer 4: Disable Built-in Plugins (~20ms savings)
local disabled_builtins = {
"gzip",
"zip",
"zipPlugin",
"tar",
"tarPlugin",
"getscript",
"getscriptPlugin",
"vimball",
"vimballPlugin",
"2html_plugin",
"logipat",
"rrhelper",
"spellfile_plugin",
"matchit",
"matchparen",
"netrw",
"netrwPlugin",
}
for _, plugin in pairs(disabled_builtins) do
vim.g["loaded_" .. plugin] = 1
end
Layer 5: Deferred Configuration (~30ms savings)
-- init.lua
vim.defer_fn(function()
require('config.options').setup()
require('config.keymaps').setup()
require('config.autocmds').setup()
end, 0)
Layer 6: Event-Based Plugin Loading (Variable)
| Loading Strategy | When Loaded | Best For |
|---|---|---|
lazy = true |
On demand | Default for all plugins |
event = "VeryLazy" |
After UI | UI enhancements |
event = "BufReadPre" |
Opening files | Treesitter, gitsigns |
event = "InsertEnter" |
Start typing | Completion, autopairs |
cmd = "Command" |
On command | Heavy tools |
ft = "filetype" |
On filetype | Language plugins |
keys = {...} |
On keypress | Motion plugins |
Lazy.nvim Performance Config
require("lazy").setup({
-- Plugin specs...
}, {
defaults = {
lazy = true, -- Lazy load by default
version = false, -- Use latest commits
},
performance = {
cache = {
enabled = true,
ttl = 3600 * 24 * 7, -- 1 week cache
},
reset_packpath = true,
rtp = {
reset = true,
disabled_plugins = {
"gzip", "matchit", "matchparen", "netrwPlugin",
"tarPlugin", "tohtml", "tutor", "zipPlugin",
},
},
},
})
Profiling Tools
Lazy Profile
:Lazy profile
Shows:
- Total startup time
- Time per plugin
- Loading order
- Event triggers
Startup Time Breakdown
# From command line
nvim --startuptime startup.log
# View results
nvim startup.log
Memory Usage
-- Current memory (KB)
:lua print(collectgarbage("count"))
-- Force garbage collection
:lua collectgarbage("collect")
:lua print(collectgarbage("count"))
Plugin Loading
:Lazy " Show all plugins and status
:Lazy health " Check lazy.nvim health
:Lazy profile " Detailed timing
GC Optimization
-- lua/config/performance.lua
M.setup = function()
-- Aggressive GC during startup
collectgarbage("setstepmul", 200)
-- Relax GC after startup
vim.api.nvim_create_autocmd("User", {
pattern = "VeryLazy",
callback = function()
collectgarbage("setstepmul", 100)
end,
})
end
Plugin-Specific Optimizations
Treesitter
{
"nvim-treesitter/nvim-treesitter",
event = { "BufReadPost", "BufNewFile" },
opts = {
highlight = {
enable = true,
disable = function(lang, buf)
-- Disable for large files
local max_filesize = 100 * 1024 -- 100 KB
local ok, stats = pcall(vim.loop.fs_stat, vim.api.nvim_buf_get_name(buf))
if ok and stats and stats.size > max_filesize then
return true
end
end,
},
},
}
Telescope
{
"nvim-telescope/telescope.nvim",
cmd = "Telescope", -- Only load on command
dependencies = {
{
"nvim-telescope/telescope-fzf-native.nvim",
build = "make", -- Compiled for performance
},
},
}
LSP
-- Debounce diagnostics
vim.lsp.handlers["textDocument/publishDiagnostics"] = vim.lsp.with(
vim.lsp.diagnostic.on_publish_diagnostics, {
update_in_insert = false, -- Don't update in insert mode
virtual_text = {
severity = { min = vim.diagnostic.severity.WARN },
},
}
)
Completion
{
"saghen/blink.cmp",
event = "InsertEnter", -- Only on insert
opts = {
trigger = {
completion = {
debounce_ms = 100, -- Debounce completions
},
},
},
}
Runtime Optimizations
Reduce Redraws
vim.opt.lazyredraw = true -- Don't redraw during macros
Syntax Optimization
vim.opt.synmaxcol = 240 -- Only highlight first 240 columns
Fold Optimization
-- Use treesitter folding with fallback
vim.opt.foldmethod = "expr"
vim.opt.foldexpr = "nvim_treesitter#foldexpr()"
vim.opt.foldlevelstart = 99 -- Start with all folds open
Timeout Optimization
vim.opt.updatetime = 250 -- Faster CursorHold
vim.opt.timeoutlen = 300 -- Faster key sequence
Large File Handling
vim.api.nvim_create_autocmd("BufReadPre", {
callback = function(args)
local ok, stats = pcall(vim.loop.fs_stat, args.file)
if ok and stats and stats.size > 1024 * 1024 then -- 1MB
-- Disable expensive features
vim.cmd("syntax off")
vim.opt_local.foldmethod = "manual"
vim.opt_local.spell = false
vim.opt_local.swapfile = false
vim.opt_local.undofile = false
end
end,
})
Monitoring Performance
Create Performance Report
local function performance_report()
local stats = {
startup_time = vim.fn.reltimefloat(vim.fn.reltime(vim.g.start_time)) * 1000,
memory = collectgarbage("count"),
buffers = #vim.fn.getbufinfo({ buflisted = 1 }),
windows = #vim.api.nvim_list_wins(),
plugins = require("lazy").stats().count,
loaded = require("lazy").stats().loaded,
}
print(string.format([[
Performance Report:
Startup: %.2fms
Memory: %.2f KB
Buffers: %d
Windows: %d
Plugins: %d/%d loaded
]], stats.startup_time, stats.memory, stats.buffers,
stats.windows, stats.loaded, stats.plugins))
end
vim.api.nvim_create_user_command("PerformanceReport", performance_report, {})
Troubleshooting Slow Startup
Profile with Lazy:
:Lazy profileCheck for slow plugins: Look for plugins taking >10ms in profile
Verify lazy loading:
:LazyCheck plugins show "not loaded" until used
Check startup log:
nvim --startuptime /tmp/startup.log && cat /tmp/startup.logIdentify culprits:
- Look for
require()calls in init.lua - Check for synchronous operations
- Look for large file reads
- Look for
Best Practices
- Always lazy load - Set
lazy = trueas default - Use events wisely -
VeryLazyfor UI,BufReadPrefor editing - Defer non-critical - Use
vim.defer_fn()for setup that can wait - Profile regularly - Check
:Lazy profileafter changes - Avoid sync operations - Use async where possible
- Limit startup plugins - Only core/theme need to load at start
- Cache aggressively - Let
vim.loaderdo its job