Lua plugin startup time

I think the move to lua plugin is overwhelmingly positive: I love having fast Lua code at runtime. However, some plugins seem to slow down nvim startup significantly. Maybe this is a price to pay, but perhaps there is something we can do about this, in particular by using more of the lazy loading we have in VimScript already.

In VimScript

let g:param = 1

And the parameter is stored even if the plugin is never loaded. Once we do load the plugin (for instance with ftplugin), it gets its parameter by reading g:param.

In Lua

The convention to write plugins appears to be:

require'my_plugin'.setup({param = 1})

and then the init.lua of this particular plugin reads:

local sub1 = require 'nested/sub/module1'
local sub2 = require 'nested/sub/module2'
local sub3 = require 'nested/sub/module3'
local sub4 = require 'nested/sub/module4'
local sub5 = require 'nested/sub/module5'
local sub6 = require 'nested/sub/module6'
…

function setup(opts)
  sub1.param = opts.param
end

function another()
    sub2.f()
    sub3.f()
    sub4.f()
    sub6.f()
end

function yet_another()
    sub2.f()
    sub5.f()
    sub6.f()
end

and so we end up loading all the plugin’s code (or most of it), at startup, and we don’t know if we will even use that. But removing the requires at the top of the file means multiple cumbersome imports with require '…/…/…' further down the file, in this example in functions yet_another and another.

Best practices/Documentation?

I couldn’t find documentation or even written best practice to write Lua plugins so that they are fast at startup. Ideally, plugins would load just the minimum (i.e. store the opts passed to setup) and then load some more modules lazily at they get used, but there doesn’t seem to be an obvious way.

Currently most Lua plugins are behaving properly in that they do not load themselves unless you call the setup function. This is an improvement compared to many vimL plugins that use to load themselves and set default mappings.

The advantage of this is that is in your control when to call the setup function. So you load the plugin only when you need it. For this purpose I recommend you use the packer plugin manager. Which comes with lazy loading capabilities. Like loading a plugin when said autocmd happens, or a vim cmd, etc.

Most Lua plugins are not using the same style of configuration as vimL plugins. let g:my_var is a global variable, and I believe most people prefer not having a ton of global variables for configuration!

You can read all the features of packer.

Here’s also a guide/tips somebody wrote on reddit.

Also the point you make about comparing vimL capabilities with what we have in Lua is the same.

In vimL you declare the global variables, when the plugin should be loaded (determined by them, maybe because of an autocmd) some function of the plugin reads the global variables and loads it self.

In Lua plugins avoid using global variables for config. But its now up to you. Think when you want the plugin to be loaded, and make the call to the plugin setup functions with your parameters when you want to. :smiley:

Soon some plugins will start lazy loading themselves so when you call setup they setup the basics, and when needed load the rest, this is a process while people learn the best way to write lua plugins.

Right.

I should have mentionned that I was aware of packer and I was wondering about the native features in NeoVim to avoid loading the a lot of Lua code just for the setup.

I guess that answer my question about the native features :slight_smile:

Thanks, that’s interesting!

1 Like

This is true but not the whole story, especially for more complex plugins. One particular point is that require has a non-negligible cost for each new module due to caching. (Later uses of the same module are fast, of course.)

For example, while Telescope is configured via require'telescope'.setup() (caching the telescope module), the individual “finders” are accessed through telescope.builtin – which is a different module. The upshot is that if you map, say, require'telescope.builtin'.find_files() in your config, you pay that loading cost during startup.

However, this does not happen if you instead map the vim command :Telescope find_files – so Telescope does support lazy-loading, but only by taking the scenic route through vimscript… So there’s still room for developing similar strategies in Lua.

1 Like

I’m pretty sure mappings are evaluated when they are invoked, not when they are defined. :smiley: The issue lays in plugins requiring all their sub modules when initializing rather than on demand.

Yeah, that was obviously complete nonsense. I can’t reconstruct what led me to convince myself that there was a difference.

The broader point of avoiding unneeded requires still stands.

The issue lays in plugins requiring all their sub modules when initializing rather than on demand.

And that is of course a very good point; see async: require plenary.async -> plenary.async.async · lewis6991/gitsigns.nvim@d77ff34 · GitHub (although that issue is being dealt with by feat: lazy load more of async by tjdevries · Pull Request #205 · nvim-lua/plenary.nvim · GitHub).

1 Like

Also teej just released a plugin that can help a lot, more control for the user. @cjoly

3 Likes

This is great, some flavor of this plugin might be considered for inclusion in nvim core at some point!

I have noticed the same thing and I think this is simply a matter of the Lua plugin ecosystem not yet reaching the level of maturity that the VimL plugin ecosystem has.

A well-written Vim plugin uses autoloaded functions as much as possible and only does what is absolutely necessary in the plugin/ directory (e.g. defining <Plug>s or setting up autocommands). In contrast, many Lua plugins require() almost their entire code base in their plugin/ directory, or, as you mentioned, they use a setup() function that also requires() a large bulk of the other code that does not need to be used simply for configuration.

VimL plugins tend to use global variables for their configuration, e.g.

let g:my_plugin_foo = 1
let g:my_plugin_bar = "baz"

This is extremely fast since it doesn’t need to load any of the plugin’s autoloaded functions. Conversely, many Lua plugins are configured like this:

require("plugin").setup({foo = 1, bar = "baz"})

This require()s all of the plugin’s Lua source code, which often require()s more files and ends up loading the entire plugin just to configure some settings, before the plugin is actually even used.

The best solution, I think, is for Lua plugin authors to re-learn the lessons of well-written Vim plugins of old and defer loading everything as long as possible. This will, I think, become more common as the ecosystem matures, but for now we have a huge influx of new plugin authors who are very talented Lua coders, but who are perhaps unfamiliar with some of the tricks used by Vim plugin authors to minimize loading time.

I do think some kind of community documentation on how to write idiomatic Neovim plugins in Lua would be hugely beneficial. Does anyone know if such a thing already exists?

2 Likes

I enfatically agree! Better documentation on this specific topic would be very useful. There’s some effort in this line of thought.

  1. Plugin Spec: This would include recommendation on how to structure the plugins. Formalize an extension specification: make configuring neovim as easy as vscode · Issue #14375 · neovim/neovim · GitHub

  2. Plugin Template: I’d imagine this will go hand and hand with number 1. GitHub - nvim-lua/nvim-lua-plugin-template: A starter template for a Neovim plugin written in Lua

  3. Nanotee Lua guide has a ton of information. GitHub - nanotee/nvim-lua-guide: A guide to using Lua in Neovim

1 Like