Настроенный под себя редактор с тремя десятками плагинов рискует превратиться в неторопливого тяжеловеса. Каждый плагин при старте читает свой код, выполняет настройку, цепляется к событиям - и вот уже запуск, который должен быть мгновенным, растягивается на заметную паузу. Особенно обидно, что большая часть этих плагинов при старте вообще не нужна: подсветка определённого языка пригодится, лишь когда откроется файл этого языка, а файловый менеджер - лишь когда его вызовут. Зачем грузить их все сразу?

Ровно эту проблему решает современный менеджер плагинов lazy.nvim. Его главная идея - ленивая загрузка: плагин читается и инициализируется не при старте, а ровно в тот момент, когда впервые понадобился. Открыли файл определённого типа - загрузился плагин для него. Нажали сочетание - подтянулся плагин, за ним стоящий. В остальное время плагин не тратит ни миллисекунды стартового времени. Разберём, какие триггеры запускают загрузку, как откладывать плагины по типу файла, и что откладывать ни в коем случае нельзя.

Четыре триггера определяют, когда плагин проснётся

Ленивая загрузка держится на триггерах - условиях, при наступлении которых плагин загружается. Их четыре основных. Триггер события привязывает загрузку к моменту вроде открытия файла или входа в режим вставки. Триггер типа файла грузит плагин при открытии файла определённого языка. Триггер команды откладывает загрузку до первого вызова конкретной команды. Триггер сочетания клавиш подтягивает плагин при первом нажатии заданной комбинации.

-- загрузка при первом вызове команды
{ "stevearc/oil.nvim", cmd = "Oil" }

-- загрузка при открытии файла на языке lua
{ "folke/lazydev.nvim", ft = "lua" }

Важнейшая тонкость: указание любого из этих триггеров само по себе означает ленивую загрузку. Не нужно отдельно помечать плагин как ленивый - наличие триггера события, типа файла, команды или сочетания уже включает отложенную загрузку. Менеджер понимает, что раз задано условие пробуждения, значит, до него плагин грузить не надо.

Механизм работает прозрачно для пользователя. Если плагин отложен до вызова команды, эта команда до загрузки как бы не существует - попытка вызвать незагруженный плагин напрямую провалится с сообщением об отсутствии команды. Но когда команда указана как триггер, менеджер перехватывает её первый вызов, загружает плагин и выполняет команду. То же с сочетаниями клавиш: заданные как триггер, они не подведут, даже если плагин ещё не загружен, - первое нажатие сначала подтянет плагин.

-- сочетание как триггер: первое нажатие загрузит плагин и выполнит команду
{
  "nvim-neo-tree/neo-tree.nvim",
  keys = {
    { "<leader>ft", "<cmd>Neotree toggle<cr>", desc = "файловое дерево" },
  },
}

Фильтрация по типу файла грузит плагин лишь для нужного языка

Триггер типа файла заслуживает отдельного разбора, потому что он закрывает самый частый случай напрасной загрузки. Плагин, полезный только для одного языка, не имеет смысла грузить при открытии файла любого другого. Привязка к типу файла откладывает его до тех пор, пока не откроется файл именно того языка.

-- плагин подсветки и подсказок для определённого языка
{ "плагин-для-языка", ft = "python" }

-- можно указать несколько типов файлов сразу
{ "плагин-веб", ft = { "html", "css", "javascript" } }

Логика прозрачна. Открываешь файл на python - менеджер видит совпадение по типу файла и загружает плагин. Открываешь файл другого языка - плагин остаётся спящим, не тратя стартового времени. Это особенно ощутимо в конфигурации, обслуживающей много языков: вместо того чтобы грузить все языковые плагины разом при старте, каждый просыпается лишь под свой язык.

Привязка по типу файла удобна не только для языковых плагинов, но и для инструментов, осмысленных лишь в определённом контексте, - например, помощников, работающих только с конкретным форматом. Чем точнее задан тип файла, тем меньше напрасной работы при старте.

Библиотеки откладывают через пометку ленивости

Не все плагины загружаются по триггерам. Многие служат библиотеками, на которые опираются другие плагины. Грузить их при старте незачем - они нужны лишь тогда, когда зависящий от них плагин их затребует. Для таких случаев есть прямая пометка ленивости.

-- библиотека загрузится, лишь когда её затребует другой плагин
{ "вспомогательная-библиотека", lazy = true }

Помеченный так плагин не загрузится сам по себе, а дождётся момента, когда другой код обратится к нему. Это удобно для зависимостей: основной плагин при загрузке сам подтянет нужную библиотеку, а до тех пор она будет спать. Пометка ленивости - явный способ сказать менеджеру, что плагин не должен просыпаться по расписанию, только по требованию.

Событие VeryLazy откладывает то, что не нужно мгновенно

Есть особое событие, придуманное именно для оптимизации старта, - оно наступает уже после того, как редактор отрисовался и стал отзывчивым. Плагины, привязанные к нему, грузятся чуть позже основного запуска, не задерживая появление редактора на экране.

-- строка состояния не нужна в первую миллисекунду старта
{ "плагин-строки-состояния", event = "VeryLazy" }

Это простейшая оптимизация для плагинов, которые нужны, но не мгновенно. Строка состояния - типичный пример: пользователь всё равно не смотрит на неё в момент появления редактора, так что её загрузку можно сдвинуть на чуть позже. Стоит, правда, понимать, что привязанный к этому событию плагин всё равно загружается при каждом старте, просто после отрисовки. Это ленивость скромная - она убирает плагин из критического пути запуска, но не отменяет загрузку вовсе, в отличие от триггеров команды или типа файла.

Профилировщик показывает, что именно тормозит старт

Чтобы оптимизировать осмысленно, а не наугад, у менеджера есть встроенный профилировщик. Он показывает, сколько времени отнял каждый плагин при старте и какие из них вышли за разумные пороги. Это превращает оптимизацию из гадания в измеримый процесс: видно, кого стоит отложить, а кто и так не мешает.

:Lazy profile     показать вклад каждого плагина в стартовое время

Рабочий подход таков: сначала измерить базовое время старта, затем через профилировщик найти плагины, отнимающие больше разумного порога, отложить их подходящим триггером и перемерить. Так шаг за шагом стартовое время снижается, а каждое изменение подтверждается замером. Профилировщик заодно показывает, какие плагины загружаются обязательно при старте и не могут быть отложены без поломки редактора, - их трогать не нужно.

Что откладывать нельзя ни в коем случае

Тут стоит провести чёткую границу, потому что неосторожная ленивость ломает внешний вид и поведение редактора. Несколько вещей откладывать категорически не следует.

Цветовую схему нужно грузить сразу и с высоким приоритетом - иначе редактор на миг мелькнёт стандартными цветами, прежде чем применит тему, и появится неприятное мерцание. Механизм разбора кода для подсветки тоже грузят при старте, иначе подсветка запоздает. Строку состояния многие откладывают через позднее событие, но не дальше - её слишком поздняя загрузка тоже заметна. И ядро настройки языковых серверов должно подняться вовремя, чтобы анализ кода был готов к моменту, когда он понадобится.

-- цветовую схему грузим сразу, с высоким приоритетом, без ленивости
{ "тема", priority = 1000, lazy = false }

Принцип прост: всё, что влияет на первый кадр или на немедленное поведение, откладывать нельзя. Мерцание цветов, запоздавшая подсветка, неготовый анализатор - цена за неуместную ленивость. А вот всё, что нужно лишь по случаю - команды, языковые плагины, файловые менеджеры, - откладывать стоит и нужно.

Что складывается в практику

Картина выстраивается ясно. Ленивая загрузка через современный менеджер плагинов держится на четырёх триггерах - событие, тип файла, команда, сочетание клавиш, - и любой из них сам по себе включает отложенную загрузку. Привязка по типу файла грузит языковой плагин лишь под свой язык, что особенно экономит на многоязычных конфигурациях. Библиотеки откладывают прямой пометкой ленивости, а особое позднее событие убирает из критического пути старта плагины, нужные не мгновенно.

Профилировщик превращает оптимизацию в измеримый процесс: найти тяжёлых, отложить, перемерить. А граница ленивости проходит по первому кадру - цветовую схему, разбор кода, строку состояния и ядро языковых серверов откладывать нельзя, иначе появятся мерцание и запаздывание.

Главная мысль: ленивая загрузка - это не отказ от плагинов, а их пробуждение точно в нужный момент. Тридцать плагинов, грузящихся при старте, тормозят запуск, а те же тридцать, привязанные к триггерам, загружаются по одному ровно тогда, когда понадобились, и старт остаётся мгновенным. Стоит освоить четыре триггера, измерить старт профилировщиком и не трогать то, что влияет на первый кадр, и богатая конфигурация перестаёт быть медленной - каждый плагин ждёт своего часа, не мешая остальным.