The Neovim plugin ecosystem has always had a tension at its center. On one side: powerful third-party managers like lazy.nvim with lazy loading, lockfiles, profiling, and automatic installation. On the other: Vim’s native package system, which has existed since Vim 8 and requires nothing but git and directory conventions. For years, reaching for a plugin manager felt like the obvious choice. vim.pack changes that calculus.
A brief history of Vim plugin management
Before Vim 8, there was no official package support. Tim Pope’s Pathogen was the first practical solution: it taught Vim to load plugins from a single ~/.vim/bundle/ directory, which you populated yourself. Then came Vundle, introducing the idea of declaring plugins in your config and having a tool handle installation. vim-plug refined this with a cleaner API and parallel downloads. Packer.nvim brought the paradigm to Neovim’s Lua world, and lazy.nvim eventually emerged as the community’s dominant choice, with a sophisticated UI and fine-grained control over when plugins load.
Vim 8 (2016) added the native :help packages system, which Neovim inherited. Packages live under packpath in a two-tier structure: pack/{name}/start/ for plugins that load automatically at startup, and pack/{name}/opt/ for plugins you load explicitly with :packadd. No configuration needed, no external code. Clone a repo into the right directory and Neovim loads it.
The problem was that this native system had no real Lua API. You could set vim.opt.packpath and call vim.cmd.packadd('plugin-name'), but there was no structured way to declare dependencies, handle initialization order, or inspect the loaded state. Plugin managers filled that gap and kept adding features until they became substantial infrastructure.
What vim.pack provides
vim.pack is a built-in Lua module in Neovim that wraps the native package system with a first-class API. Rather than constructing packpath entries by hand or issuing :packadd calls through vim.cmd, you get a declarative interface:
-- Load a plugin at startup with configuration
vim.pack.add('echasnovski/mini.nvim', {
config = function()
require('mini.pairs').setup()
require('mini.comment').setup()
end,
})
-- Load lazily, triggered by a command
vim.pack.add('nvim-telescope/telescope.nvim', {
lazy = true,
cmd = { 'Telescope' },
depends = { 'nvim-lua/plenary.nvim' },
})
The critical distinction: vim.pack does not download anything. Plugin files still need to be present on disk, placed via git clone, git submodule, or whatever approach you already use. What vim.pack provides is a Lua-native way to declare how Neovim should load what is already there, including lifecycle hooks and dependency ordering.
echasnovski’s guide covers the full API and explains how each option maps to the underlying packpath mechanics. It is worth reading carefully if you want to understand what the abstraction is doing rather than treating it as just another configuration format.
The underlying mechanics
vim.pack sits on top of Neovim’s packpath resolution. By default, Neovim scans ~/.local/share/nvim/site on Linux (and equivalent paths on other platforms) for pack/*/start/ and pack/*/opt/ directories.
Plugins in start/ are sourced before your init.lua runs. Their plugin/ files, ftplugin handlers, and syntax definitions get registered unconditionally as part of startup. Plugins in opt/ sit dormant until explicitly requested. Every plugin in start/ adds to the baseline cost of opening Neovim, which is why lazy loading exists.
vim.pack’s lazy = true option uses opt/ semantics, deferring the plugin until a trigger fires: a command invocation, a filetype event, or a Neovim event like BufReadPre. Without lazy, the plugin loads at startup. The config callback runs after the plugin loads; the init callback, if provided, runs before, letting you set global variables or keymaps before the plugin sources its own files.
The depends field handles ordering. If plugin B lists plugin A as a dependency, vim.pack ensures A is loaded and configured before B’s config runs. This is the piece that raw packpath management makes awkward to express cleanly, and it is what previously pushed people toward plugin managers even when they did not need the download management.
Compared with lazy.nvim
lazy.nvim is genuinely excellent at what it does. The built-in UI lets you inspect which plugins loaded, how long each took, and what triggered the load. The lockfile pins exact commit hashes, giving you reproducible environments across machines. The lazy loading granularity goes down to specific Lua module require calls, which is difficult to replicate by hand.
For configurations with 60 or more plugins loading on a variety of conditions, that complexity is appropriate and the tooling earns its presence. vim.pack fits a different set of goals.
If you already manage plugins as git submodules in your dotfiles, vim.pack slots in cleanly. Installation and updates are your own concern; you just need a clean declaration of how Neovim should load what is on disk. A minimal setup with plugins in ~/.local/share/nvim/site/pack/plugins/:
PACK_START=~/.local/share/nvim/site/pack/plugins/start
PACK_OPT=~/.local/share/nvim/site/pack/plugins/opt
mkdir -p $PACK_START $PACK_OPT
git clone https://github.com/nvim-treesitter/nvim-treesitter $PACK_START/nvim-treesitter
git clone https://github.com/nvim-lua/plenary.nvim $PACK_START/plenary.nvim
git clone https://github.com/nvim-telescope/telescope.nvim $PACK_OPT/telescope.nvim
Updates are git pull in each directory, with no lock files, no manager UI, and no sync command to remember. For teams that already treat their dotfiles as a proper repository, this model fits naturally.
The startup time comparison deserves a direct answer. For configs with under 20 focused plugins, the difference between vim.pack and lazy.nvim’s lazy loading is likely negligible. For configs with 80 or more plugins loading on various conditions, lazy.nvim’s profiling and fine-grained triggers are harder to replicate. vim.pack covers the common cases well; it does not replicate every optimization lazy.nvim provides, and it does not try to.
What this signals about Neovim’s direction
vim.pack is part of a broader pattern in the project. The same codebase that gave us vim.lsp (a complete built-in LSP client), vim.treesitter (native tree-sitter integration), and vim.iter (functional iteration over tables and iterators) is now providing native package management with a proper Lua API. Each of these additions reduced the surface area where community plugins needed to fill gaps that Neovim itself should cover.
Five years ago, a Neovim installation without a substantial plugin ecosystem was barely usable for serious development work. Today, the built-in LSP client, treesitter integration, and native package management make the core installation substantively more capable on its own. Plugins extend what is there rather than construct the foundation from scratch.
Whether this pushes the broader community toward leaner configs is an open question. lazy.nvim has strong inertia and its convenience features are real. But for the contingent of users who want a well-understood, dependency-minimal setup, vim.pack is a meaningful improvement over the previous options: either adopt a full plugin manager or write your own packpath wrangling code. Neovim now provides a third path, built into the editor itself, and that is the kind of addition that slowly changes how people think about what a reasonable baseline configuration looks like.