Improving Neovim Startup Time Using Lazy Load

Suggest An Edit

Table of Content

Introduction

20-08-2021: This post is no longer maintained because I’ve changed my config quite a bit since I wrote this and I don’t feel like updating it :p

I’ve been using Neovim with a lot of plugins and the startup time is horrible. I have around 50 plugins and it took roughly 300ms to load up. Still quite fast compared to modern editors but it’s definitely slower than a plain Neovim without any plugin.

I’ve found a way how to solve this problem by lazy loading almost most of my plugins, I thought it’s a good idea to write it here in case people want to do the same.

Prerequisite

To do this, you need a package manager that supports lazy loading. I’m using packer.nvim, I don’t know about the other package manager, but the core concept is just “load the plugin on a certain event” rather than “load everything on startup”.

NOTE: I will be omitting irrelevant parts of the config for the sake of brevity

DISCLAIMER: THESE TIPS MAY OR MAY NOT WORK FOR YOU. PLEASE DON’T LAZYLOAD EVERYTHING BLINDLY, IT CAN MESS UP YOUR CONFIG.

UI-related Plugins

DevIcons

I have nvim-web-devicons installed and a few plugins depend on it. I use the module field to let it load only when a plugin require it.

Here’s a snippet on how I load it:

{
  "kyazdani42/nvim-web-devicons",
  module = "nvim-web-devicons",
}

Utility-related Plugins

vim-easy-align

vim-easy-align is quite a handy plugin to have, it can align stuff to make it look nicer. This plugin gets triggered by <Plug>(EasyAlign). It looks like a cmd but it actually belongs to keys because it’s a mapping.

{
  "junegunn/vim-easy-align",
  keys = "<Plug>(EasyAlign)",
}

This way, the plugin won’t be available unless I trigger it.

Telescope

Similar to the previous plugin, I trigger telescope.nvim using the keys field so it will get loaded only if I want it. But, I also have the module field which tells packer to load this plugin whenever it gets required by another module.

{
  "~/repos/telescope.nvim",
  module = "telescope",
  keys = {
    {"", "<C-p>"},
    {"", "<C-f>"},
    {"n", "<Leader>f"}
  },
}

I use <C-p> to trigger find_files and <C-f> to trigger grep_string, the rest prefixed with <Leader>f.

Plenary

Some of my plugins depends on plenary.nvim but plenary itself is just a module so it makes sense to load it only when something needs it. Again, I’m using module key for this.

  {
    "nvim-lua/plenary.nvim",
    module = "plenary"
  },

vim-test

Since vim-test is triggered by executing a command, I can use it to lazy-load this plugin by doing so:

{
  "vim-test/vim-test",
  cmd = { "TestFile", "TestNearest", "TestSuite", "TestVisit" },
}

The cmd could also be a string if there’s only 1 item.

Language-related Plugins

I have several plugins for better language support such as vimtex for latex and vim-markdown for markdown. I load them only on certain filetypes.

Here’s a snippet for vim-markdown:

{
  "plasticboy/vim-markdown", -- or "lervag/vimtex"
  filetype = "markdown", -- or "latex"
  setup = function()
    vim.g.vim_markdown_folding_disabled = 1
    vim.g.vim_markdown_frontmatter = 1
  end
}

As you can see, I do the config inside the setup key instead of config key. This makes those global variables get set before the plugin gets loaded, otherwise it won’t affect the plugin.

LSP-related Plugins

I made LSP-related plugins to be loaded on BufRead event or specific filetype, because it gets triggered after a file gets loaded into a buffer. I sometimes open Neovim as a scratch which doesn’t load any file to a buffer so these plugins won’t get loaded.

Here’s some example from my config

Flutter, Rust, Java, and Typescript servers

I load them using a filetype because they have their own ‘extension’, so to speak. I don’t use the one provided in nvim-lspconfig because these servers have some special functionalities which can only be achieved using some extra implementation rather than just ‘starting the server’ like inlay hints, widget guides, etc.

Here’s a snippet:

{
  "simrat39/rust-tools.nvim", -- or "akinsho/flutter-tools.nvim", etc
  ft = "rust", -- this is the important field, adjust them to the appropriate filetype
}

If I open any filetype that doesn’t match them, they won’t get loaded, reducing the time needed to open Neovim.

UPDATE Tue, July 20, 2021

After doing this for a while, yeah, it’s not a good idea :p

Sometimes it gets wonky.

Completion and Snippet

UPDATE Fri, August 13, 2021

I now moved to nvim-cmp from the same author and it does some lazy loading internally so I don’t need to do that anymore.

I use nvim-compe for autocompletion. I load it on the InsertEnter event. It only makes sense to load an autocompletion plugin after I go to Insert Mode. Sometimes, I open Neovim just to look at a file, move around, never get into Insert Mode, and quit Neovim. For this reason, always loading nvim-compe will be redundant.

Here’s a snippet for it:

{
  "hrsh7th/nvim-compe",
  event = "InsertEnter",
  requires = {
    require("plugins.luasnip").plugin
  }
}

If you noticed, I have a requires field. I like to structure my plugin definition to be linked to each other if a plugin requires another plugin, in this case, it’s LuaSnip.

DAP Client

I have nvim-dap installed for debugging, since I trigger its functionality using <Leader>d prefix – <Leader>db to add a breakpoint, for example – so I load it only when I press <Leader>d. Packer has a field called keys to place the keybind that triggers this plugin.

{
  "mfussenegger/nvim-dap",
  keys = "<Leader>d",
}

Tips

Loading Configuration

Since we’re loading them on a certain event, their config needs to get loaded after the plugin has been loaded. Fortunately, packer has a feature where you can specify the configuration for the plugin. If you’re trying to load the configuration separately, you might load the configuration before the plugin gets loaded which will cause an unwanted error.

We can do it by using the config field provided by packer. If it’s a huge config, I’d recommend putting it to another file and do require("your.config") inside the config function like so:

{
  "you/your-cool-plugin",
  config = function()
    require("your.config")
  end
}

The reason is everything inside packer definition will get compiled into packer_compiled.vim so if you want to update your config inside that config field, you’ll need to recompile every time. This way, you don’t need to do that, you can just edit your/config.lua file.

Disabling Built-In Plugins

If you want more speedup, I’d recommend disabling the builtin vim plugins – or don’t if you’re using them – like so: This won’t add that much, but I like to disable them since I’m not using them anyway.

vim.g.loaded_gzip         = 1
vim.g.loaded_tar          = 1
vim.g.loaded_tarPlugin    = 1
vim.g.loaded_zipPlugin    = 1
vim.g.loaded_2html_plugin = 1
vim.g.loaded_netrw        = 1
vim.g.loaded_netrwPlugin  = 1
vim.g.loaded_matchit      = 1
vim.g.loaded_matchparen   = 1
vim.g.loaded_spec         = 1

Only Load Packer When Needed

If you don’t need packer all the time, just mark it as opt and add this line at the top of the file.

vim.cmd [[ packadd packer.nvim ]]

If you need packer, you’ll go to that file and do luafile %, boom, packer gets loaded and you can do packer related stuff.

References

Here are some references that I’ve used:

Closing Notes

Those are not all of my plugins obviously, it would take forever to go through 50 plugins I used :p

Here’s my startup time – tested using startuptime.vim – for reference.

UPDATE Wed, July 7, 2021

I’ve decreased the amount of lazy loading because sometimes they can cause some issue with autocmd

  • Before (fully) lazy loading:

    Total Time:  219.446 -- Outstanding
    
    Slowest 10 plugins (out of 30)~
                        [vimrc]	 173.742
                            icy	 17.158
                      [runtime]	 11.249
                     nvim-compe	 5.735
                    vim-matchup	 4.569
    nvim-treesitter-textobjects	 2.347
                   vim-nonicons	 1.997
                  vim-sandwhich	 1.158
                nvim-treesitter	 0.995
                  splitjoin.vim	 0.496
  • After lazy loading:

    Total Time:   93.796 -- Flawless Victory

Slowest 10 plugins (out of 18)~ packer.nvim 50.963 [runtime] 12.193 vim-matchup 10.051 [unknown] 8.848 [vimrc] 6.048 nvim-treesitter-textobjects 1.599 icy 1.500 nvim-treesitter 1.361 LuaSnip 0.478 nvim-ts-context-commentstring 0.165


You can find the list of my plugins [here][plugins-link]. Anyway, hope you find something useful from this post and have a nice day! ツ

Comments