Carga perezosa desde dentro: no hacer trabajo al cargar
Por qué la pereza es responsabilidad del plugin y no solo del gestor: mantener plugin mínimo, entender qué cuesta realmente un require, diferir módulos con proxies y cierres, elegir puntos de entrada perezosos y medir el arranque en lugar de suponerlo.
Existe un reparto de responsabilidades tácito que casi todo el mundo entiende al revés. Se asume que la carga perezosa es asunto del gestor de plugins, que el usuario la configura con un evento o un comando en su spec y que el autor no tiene nada que hacer al respecto. Pero el gestor solo puede diferir aquello sobre lo que tiene control, es decir, el instante en que inserta tu directorio en la lista de rutas; a partir de ahí, todo lo que tu código decida ejecutar es coste que el usuario paga y que ninguna configuración externa puede evitar. Un plugin que hace trabajo al cargarse impone ese trabajo a todos sus usuarios, incluidos los que abren el editor para corregir una línea y salir. La pereza, entonces, no es una optimización que se aplica al final: es una propiedad estructural que se decide en las primeras diez líneas que escribes y que consiste, en su totalidad, en aprender a no hacer nada hasta que alguien lo pida.
- Reducir
plugin/a lo estrictamente irreducible y justificar cada línea que sobreviva. - Distinguir el coste de descubrir un módulo del coste de ejecutar su cuerpo.
- Diferir la carga de submódulos con cierres y con acceso indirecto por metatabla.
- Medir el coste real de arranque con las herramientas del editor en lugar de confiar en la intuición.
plugin/ es un impuesto que pagan todos
Todo fichero bajo plugin/ se ejecuta en cada arranque, sin condición ni posibilidad de evitarlo salvo desinstalando. Es el único código tuyo que corre siempre, y por eso su contenido debe limitarse a declarar puntos de entrada: nombres de comandos, teclas y eventos que, al activarse, cargarán lo demás. Registrar un comando es escribir una entrada en una tabla interna del editor y cuesta microsegundos; cargar el módulo que lo implementa puede costar milisegundos.
-- plugin/pico.lua : el fichero completo, nada mas debe vivir aqui
if vim.g.loaded_pico or vim.fn.has("nvim-0.10") == 0 then return end
vim.g.loaded_pico = true
vim.api.nvim_create_user_command("Pico", function(o)
require("pico").abrir(o.args) -- el require ocurre al invocar
end, { nargs = "?", desc = "abre el bloc del proyecto" })
vim.api.nvim_create_autocmd("FileType", {
pattern = "markdown",
callback = function(ev) require("pico.markdown").adjuntar(ev.buf) end,
})
Tres rasgos definen un fichero de entrada correcto. La guarda impide la doble ejecución y comprueba la versión mínima antes de tocar nada. Cada llamada a require vive dentro del cuerpo de una función, nunca en el ámbito del fichero. Y no hay ninguna otra cosa: ni tablas de configuración, ni cálculo de rutas, ni lectura de ficheros, ni comprobación de ejecutables disponibles. Ese último punto es el que más se incumple: verificar con vim.fn.executable si una herramienta externa existe parece inofensivo y es una llamada al sistema en el camino crítico del arranque, multiplicada por el número de plugins que hacen lo mismo.
Qué cuesta realmente un require
Conviene separar dos costes que suelen confundirse. El primero es encontrar el fichero: el buscador recorre las entradas del runtimepath probando rutas hasta dar con la correcta, lo que implica operaciones de disco proporcionales al número de plugins instalados. El segundo es ejecutar el cuerpo del módulo, que incluye a su vez los require que ese módulo haga en su ámbito superior, en cascada. El segundo domina casi siempre, y es enteramente tuyo.
-- MAL: importar todo el arbol al cargar el modulo raiz
local ventana = require("pico.ventana")
local historial = require("pico.historial")
local exportar = require("pico.exportar") -- pesado, se usa una vez al mes
-- BIEN: acceso indirecto que carga cada submodulo la primera vez que se toca
local M = setmetatable({}, {
__index = function(t, clave)
local ok, mod = pcall(require, "pico." .. clave)
if not ok then return nil end
rawset(t, clave, mod) -- memoriza: el coste se paga una vez
return mod
end,
})
function M.abrir(args)
return M.ventana.crear(M.config.get(), args) -- carga pico.ventana aqui mismo
end
La metatabla convierte cada submódulo en perezoso sin cambiar la sintaxis de uso, y es el patrón que emplean los plugins grandes del ecosistema para exponer un árbol amplio sin pagarlo entero. Hay una alternativa aún más barata cuando el punto de uso es único: el cierre. Si una función necesita un módulo pesado, el require va en su primera línea; Lua lo resolverá en la caché a partir de la segunda invocación y el coste desaparece. Y una advertencia sobre el caché de bytecode: el editor compila y almacena los módulos Lua para acelerar cargas posteriores, lo que reduce el coste de análisis sintáctico pero no el de ejecutar el cuerpo. Un módulo que abre ficheros o consulta el sistema al cargarse es igual de caro con caché que sin él.
Comando de usuario
El punto de entrada más barato. El nombre existe desde el arranque; el módulo se carga en la primera invocación real del usuario.
Evento y tipo de archivo
Ideal para plugins ligados a un lenguaje. Nada se carga hasta que aparece un buffer de ese tipo, y muchas sesiones no lo abren jamás.
Atajo de teclado
Registra la tecla con una función anónima que hace el require en su interior. El coste se difiere hasta la primera pulsación.
Evento muy diferido
Para lo que debe existir pero no urge: se engancha a un evento posterior al arranque y no retrasa la aparición del primer buffer.
Diseñar para la pereza ajena
Tu plugin no solo debe ser perezoso: debe dejarse cargar perezosamente por el gestor del usuario. Eso impone dos obligaciones adicionales. La primera es que la funcionalidad no dependa de haberse cargado en el arranque; si tu código asume que estuvo presente cuando ocurrió cierto evento inicial, romperás en cuanto alguien lo cargue bajo demanda. La segunda es que, al cargarse tarde, el plugin debe ponerse al día con el estado que ya existe.
-- Al cargarse tarde, adjuntarse tambien a los buffers ya abiertos
function M.setup(opts)
M.options = require("pico.config").resolver(opts)
local grupo = vim.api.nvim_create_augroup("Pico", { clear = true })
vim.api.nvim_create_autocmd("FileType", {
group = grupo, pattern = M.options.filetype,
callback = function(ev) M.markdown.adjuntar(ev.buf) end,
})
-- Recuperacion: los buffers que ya cumplen la condicion no dispararan el evento
for _, buf in ipairs(vim.api.nvim_list_bufs()) do
if vim.api.nvim_buf_is_loaded(buf) and vim.bo[buf].filetype == M.options.filetype then
M.markdown.adjuntar(buf)
end
end
end
Ese bucle de recuperación es la diferencia entre un plugin que funciona y uno que funciona a partir del segundo archivo, un fallo tan característico de la carga diferida que conviene reconocerlo de inmediato. La regla general se enuncia así: cualquier efecto que dependa de un evento debe ir acompañado de un barrido inicial sobre el estado existente, porque el evento ya pasó para todo lo que estaba abierto.
flowchart TB a[Arranque] --> b[Solo se ejecuta plugin con guardas y registros] b --> c[Ocurre el disparador] c -->|Comando invocado| d[require del modulo principal] c -->|Filetype detectado| d c -->|Tecla pulsada| d c -->|Nunca ocurre| e[Coste total cercano a cero] d --> f[Submodulos por metatabla solo al tocarlos] f --> g[Barrido de recuperacion sobre buffers ya abiertos] style e fill:#a6e3a1,color:#11111b style b fill:#f9e2af,color:#11111b
Medir en vez de creer
La intuición sobre rendimiento es sistemáticamente incorrecta, y en el arranque de un editor el error suele ser de un orden de magnitud. Hay tres instrumentos, y conviene usar los tres porque miden cosas distintas.
# Coste absoluto por fichero cargado, ordenado de mayor a menor
nvim --startuptime /tmp/inicio.log +q && sort -k2 -nr /tmp/inicio.log | head -25
# Comparar el editor desnudo contra el editor con tu configuracion completa
nvim --clean --startuptime /tmp/limpio.log +q ; tail -3 /tmp/limpio.log
-- Dentro del editor: perfil por plugin y confirmacion de que el tuyo no se cargo
:Lazy profile
vim.print(package.loaded["pico"] ~= nil) -- debe ser falso hasta invocar el comando
:checkhealth pico
La comprobación de la última línea es la única prueba concluyente: si tras arrancar el editor tu módulo aparece en la tabla de módulos cargados sin que nadie haya pedido nada, la pereza está rota, y el motivo casi siempre es un require fuera de sitio en plugin/ o un submódulo importado en el ámbito superior del módulo raíz. Fija además un presupuesto explícito y trátalo como un requisito: un plugin de esta clase debe añadir menos de un milisegundo al arranque cuando no se usa. Es una cifra alcanzable con las técnicas de este capítulo, y expresarla como número convierte una aspiración vaga en algo que una prueba puede verificar.
Merece la pena entender por qué esta comunidad concreta se toma el arranque con una seriedad que resultaría desproporcionada en casi cualquier otro contexto de programación. La razón no es técnica sino aritmética, y tiene que ver con la asimetría entre quien escribe y quien paga. Un plugin lo escribe una persona una vez; lo ejecutan miles de personas decenas de veces al día, muchas de ellas para abrir un fichero, cambiar una palabra y salir. Cincuenta milisegundos de trabajo innecesario al cargar es una cifra que su autor jamás percibirá al probarlo, y que multiplicada por el número de instalaciones y de arranques diarios se convierte en años de espera humana agregada. Además, ese coste es acumulativo y no atribuible: el usuario con cuarenta plugins no experimenta cuarenta retrasos identificables, experimenta un editor que tarda dos segundos en abrir sin que nadie en particular sea culpable, y la culpa difusa es precisamente la que nunca se corrige. De ahí que la única defensa posible sea una norma cultural sostenida por cada autor individualmente: no hagas trabajo al cargar, aunque sea poco, aunque nadie vaya a notarlo, porque el sistema que habitas solo permanece rápido si todos sus participantes se comportan como si su parte fuera la que importa. Hay aquí una lección general sobre bienes comunes que se repite en cada plataforma compartida —el tamaño de las dependencias en un ecosistema de paquetes, los permisos que pide una aplicación móvil, las consultas que un servicio lanza contra una base de datos común— y siempre adopta la misma forma: el coste individual de portarse mal es despreciable, el coste agregado es devastador, y ninguna herramienta puede imponer la solución porque distinguir el trabajo necesario del innecesario requiere entender la intención. Escribir un plugin perezoso es, en ese sentido, menos un ejercicio de optimización que un acto de disciplina profesional hacia gente a la que nunca conocerás, y es exactamente la clase de hábito que distingue el código que la gente instala del código que la gente desinstala tras una semana.
- Reduce tu
plugin/a guardas y registros, y verifica con la tabla de módulos cargados que nada de tu árbol se carga en el arranque. - Sustituye los
requiredel ámbito superior de tu módulo raíz por acceso indirecto con metatabla y comprueba que memoriza correctamente. - Añade el barrido de recuperación sobre buffers abiertos y demuestra el fallo previo cargando el plugin a mano con un archivo ya abierto.
- Mide el arranque con y sin tu plugin instalado y fija un presupuesto por debajo del milisegundo.
- Encuentra en tu configuración el plugin más caro con el perfilador y averigua exactamente qué línea suya paga ese coste.