wandres.dev
RENDIMIENTO · un editor instantáneo

Carga perezosa de verdad: evento, comando y tipo de archivo

Los tres ejes reales de la carga diferida y como se implementan con disparadores registrados en el arranque, por que un plugin marcado como perezoso sigue costando, el autoengano de mover trabajo detras del primer pintado, el coste de las propias especificaciones y de los bloques de inicializacion, y como verificar con el perfil del gestor.

⏱ 22 min

La carga perezosa es la técnica más eficaz y peor entendida del ajuste de arranque. Todo el mundo marca sus plugins como diferidos, casi nadie sabe qué ocurre exactamente cuando lo hace, y por eso abundan las configuraciones en las que todo es perezoso y el editor sigue tardando cuatrocientos milisegundos en estar listo. La explicación no es que la técnica falle: es que diferir la carga de un plugin no es gratis, no siempre difiere lo que crees, y a veces solo traslada el gasto a un momento en el que el cronómetro ya ha dejado de contar pero el usuario sigue esperando.

🎯 Al terminar esta lección sabrás
  • Implementar diferimiento por evento, por comando, por mapeo y por tipo de archivo, sabiendo qué disparador registra cada uno.
  • Enumerar los costes que sobreviven al diferimiento y localizarlos en tu propia configuración.
  • Distinguir el arranque medido del arranque percibido y detectar el trabajo escondido tras el primer pintado.
  • Verificar el resultado con el perfil del gestor y con una medida independiente.

Los tres ejes de la pereza

Diferir consiste en no cargar el código del plugin durante el arranque y, en su lugar, registrar un disparador barato que lo cargará cuando de verdad haga falta. Los ejes disponibles se corresponden con los mecanismos de extensión del editor, y cada uno registra una cosa distinta.

return {
  -- Por evento: registra un autocomando
  { "lewis6991/gitsigns.nvim", event = { "BufReadPre", "BufNewFile" }, opts = {} },

  -- Por comando: crea una orden de usuario provisional
  { "nvim-telescope/telescope.nvim", cmd = "Telescope", opts = {} },

  -- Por mapeo: crea el mapeo provisional, con su descripcion
  { "folke/trouble.nvim", keys = { { "<leader>xx", "<cmd>Trouble<cr>", desc = "diagnosticos" } } },

  -- Por tipo de archivo: autocomando de FileType
  { "mrcjkb/rustaceanvim", ft = "rust" },

  -- Diferido sin disparador: lo cargara otro plugin que dependa de el
  { "nvim-lua/plenary.nvim", lazy = true },
}

Los disparadores son sustitutos temporales que se autodestruyen. La orden de usuario provisional, al invocarse por primera vez, se borra a sí misma, carga el plugin real —que registra su propia orden definitiva— y reenvía los argumentos originales a esa orden. El mapeo provisional hace lo mismo con la secuencia de teclas. Este truco es elegante y tiene una consecuencia que conviene tener presente: la primera invocación de cualquier plugin diferido paga la carga completa, y si esa carga son cien milisegundos, tu primera búsqueda del día se siente lenta aunque el arranque fuera instantáneo. Has movido el coste, no lo has eliminado.

💡
El evento tardío y su significado exacto

El evento sintético que los gestores llaman muy perezoso no es un evento del editor: es un autocomando que el gestor dispara él mismo poco después de que la interfaz esté lista, ya fuera del arranque contabilizado. Sirve para todo lo que debe existir pero no en el primer instante: línea de estado, notificaciones, sesiones. Lo que no debe hacer es convertirse en el cajón donde acaba todo, porque entonces el editor arranca rápido y se queda inmóvil dos décimas mientras carga veinte plugins de golpe.

Lo que la pereza no evita

Aquí está el núcleo del capítulo. Un plugin marcado como diferido sigue costando en el arranque por cuatro vías distintas, y las cuatro son medibles.

La primera es la lectura de la especificación. Para saber que un plugin es perezoso, el gestor tiene que leer su declaración, y eso significa cargar y ejecutar todos tus ficheros de especificación. Cien plugins repartidos en cincuenta ficheros son cincuenta módulos Lua cargados en cada arranque, pase lo que pase. Si además tus especificaciones hacen trabajo en el nivel superior —construir tablas grandes, consultar el sistema, llamar a require del propio plugin para leer una constante— ese trabajo se ejecuta siempre y anula el diferimiento por completo.

-- MAL: el require se ejecuta al leer la especificacion, en cada arranque
{ "un/plugin", opts = { icono = require("un.plugin.iconos").defecto } }

-- BIEN: la funcion se evalua solo cuando el plugin se carga de verdad
{ "un/plugin", opts = function() return { icono = require("un.plugin.iconos").defecto } end }

La segunda es el registro de disparadores. Crear una orden provisional, un mapeo o un autocomando cuesta poco, pero no cero, y una configuración grande registra centenares. Es un coste irreducible mientras quieras esa funcionalidad disponible, y por eso el mejor ajuste de arranque casi siempre es una poda: el plugin que no está no cuesta nada, ni siquiera diferido.

La tercera es el bloque de inicialización. Los gestores distinguen entre la configuración que se ejecuta al cargar el plugin y la que debe ejecutarse siempre, en el arranque, porque establece variables globales que el plugin leerá después. Ese segundo bloque se ejecuta por definición durante el arranque, esté el plugin diferido o no. Un plugin perezoso con un bloque de inicialización de cuarenta líneas no es un plugin perezoso.

La cuarta son las dependencias y los directorios automáticos. Un plugin diferido que declara una dependencia arrastra a esa dependencia consigo, y si algo fuerza su carga temprana, la cadena entera se despierta. Y cualquier plugin que sí se cargue trae consigo sus ficheros de arranque automático y de detección de tipos, que el editor lee por el hecho de estar donde están.

flowchart TB
a[Arranque] --> b[Lectura de todas las especificaciones]
b --> c[Ejecucion de los bloques de inicializacion]
c --> d[Registro de disparadores]
d --> e[Ordenes provisionales]
d --> f[Autocomandos por evento y por tipo de archivo]
d --> g[Mapeos provisionales]
e --> h[Carga real bajo demanda]
f --> h
g --> h
style b fill:#f38ba8,color:#11111b
style c fill:#f38ba8,color:#11111b
style h fill:#a6e3a1,color:#11111b

Los dos nodos marcados en rojo ocurren siempre. Son el suelo de tu configuración, y ninguna cantidad de diferimiento los reduce. Ese suelo se puede cuantificar, y hacerlo es una de las mediciones más reveladoras del nivel, porque convierte una intuición vaga en una cifra que crece visiblemente con cada plugin que añades.

-- Cuantos disparadores se han registrado en el arranque
print("autocomandos:", #vim.api.nvim_get_autocmds({}))
print("ordenes de usuario:", vim.tbl_count(vim.api.nvim_get_commands({})))
print("mapeos en modo normal:", #vim.api.nvim_get_keymap("n"))

El arranque medido y el arranque percibido

Existe una manera trivial de conseguir un número excelente: enganchar todo al evento tardío. El cronómetro se detiene cuando la interfaz está lista, y todo lo que ocurre después no aparece en el informe. El editor arranca en treinta milisegundos y a continuación se queda sordo doscientos más mientras carga en cascada lo que dejaste fuera del recuento. La medida es correcta y la experiencia es peor que antes.

Hay una segunda versión del mismo autoengaño, más sutil, que consiste en medir siempre en vacío. Los eventos de lectura de búfer se disparan al abrir un fichero, y si abres el editor pasándole un fichero por la línea de órdenes, eso ocurre antes de entrar en el bucle principal y por tanto sí cuenta. Un plugin diferido a la lectura de búfer no está diferido en el único caso que te importa, que es abrir un fichero.

# Estas dos cifras describen dos mundos distintos
nvim --startuptime /tmp/vacio.log +q
nvim --startuptime /tmp/real.log +q src/main.rs

La regla operativa que se deduce es sencilla de enunciar y difícil de cumplir: mide siempre abriendo un fichero representativo, y considera terminado el arranque cuando el editor responde a una tecla, no cuando el informe deja de escribir.

Verificar y podar

El gestor expone su propia contabilidad, que es la vista más directa de lo que acabamos de describir: qué plugins se cargaron durante el arranque, cuánto costó cada uno y qué los despertó.

:Lazy profile
:Lazy debug

La columna de tiempo ordena a los sospechosos; la información de qué disparó cada carga es la que produce los hallazgos. El patrón que aparece una y otra vez es el mismo: un plugin que creías diferido figura como cargado en el arranque porque otro, no diferido, lo declaró como dependencia. Deshacer esa cadena suele valer más que todas las demás optimizaciones juntas.

Hay además dos casos que no se resuelven con ninguno de los ejes anteriores y que conviene tener en el repertorio. El primero es el esquema de color, que debe cargarse antes que nada porque cualquier retraso produce un destello visible de la paleta por defecto; para eso existe una prioridad explícita que ordena la carga entre los plugins que sí arrancan. El segundo es la carga condicional, una función que se evalúa en el arranque y decide si el plugin existe siquiera para esta sesión: el criterio natural es la máquina, la presencia de un ejecutable o el hecho de estar dentro de un repositorio.

{ "catppuccin/nvim", name = "catppuccin", priority = 1000, lazy = false },
{ "un/plugin-de-git", cond = function() return vim.fn.isdirectory(".git") == 1 end },

Queda un frente que el gestor no cubre, porque es anterior a él: los plugins que el propio editor trae de serie y carga siempre. Desactivarlos es un ahorro pequeño pero perfectamente gratuito si no los usas.

-- En init.lua, antes del gestor
for _, p in ipairs({
  "netrwPlugin", "tarPlugin", "zipPlugin", "gzip", "tutor",
  "matchit", "matchparen", "rplugin", "spellfile_plugin",
}) do
  vim.g["loaded_" .. p] = 1
end
La pereza no es una técnica de rendimiento: es una teoría sobre qué necesitas y cuándo

El error de fondo en casi todas las configuraciones sobreajustadas es tratar el diferimiento como un interruptor mecánico que se aplica a todo por igual, cuando lo que en realidad estás escribiendo, cada vez que eliges un disparador, es una afirmación comprobable sobre tu propio trabajo: que este plugin no hace falta hasta que abras un fichero de este lenguaje, que aquel no hace falta hasta que pulses esta tecla, que ese otro no hace falta nunca salvo en un proyecto de cada veinte. Vista así, la configuración deja de ser una lista de dependencias y pasa a ser un modelo explícito de tus condiciones de uso, y ese modelo puede estar bien o mal, y se puede falsar. Cuando lo aciertas, el efecto excede con mucho el arranque: el editor solo tiene cargado en cada momento el código que tu tarea actual justifica, con menos autocomandos compitiendo por cada pulsación, menos memoria, menos superficie donde algo pueda romperse y menos interacciones imprevistas entre plugins que nunca coinciden en la misma sesión. Cuando lo yerras, lo notas de inmediato y de una manera que enseña, porque el retraso aparece exactamente en el punto donde tu modelo era falso. De ahí se sigue la consecuencia menos popular y más útil de todo el nivel: la respuesta correcta a un plugin caro no es casi nunca diferirlo con más astucia, sino preguntarse si lo usas. Diferir es la respuesta para lo que usas a veces; desinstalar es la respuesta para lo que instalaste una tarde por curiosidad y llevas ocho meses arrastrando en cada arranque, en cada actualización y en cada depuración. Una configuración de treinta plugins que entiendes es más rápida, más estable y más tuya que una de ciento veinte perfectamente diferidos, y el diferimiento, cuando se convierte en la excusa para no elegir, deja de ser una optimización y pasa a ser un modo elegante de acumular.

⚔️ Auditar tu grafo de carga
  1. Abre el perfil del gestor tras un arranque con un fichero real y anota todos los plugins que figuran como cargados. Para cada uno, averigua si lo pediste tú o lo arrastró una dependencia.
  2. Busca en tus especificaciones todas las llamadas a require que estén en el nivel superior o dentro de una tabla de opciones literal, y conviértelas en funciones diferidas. Mide antes y después.
  3. Localiza tus tres bloques de inicialización más largos y decide, línea a línea, cuáles necesitan de verdad ejecutarse en el arranque.
  4. Compara la marca del primer redibujado abriendo el editor en vacío y abriendo un fichero de tu lenguaje principal. Explica la diferencia con el perfil en la mano.
  5. Cuenta cuántos plugins tienes y cuántos has usado en los últimos treinta días. Desinstala los que sobren y vuelve a medir.