La vida es demasiado corta para usar una terminal lenta
(mijndertstuij.nl)- La velocidad de la terminal que usas todo el día afecta directamente tu productividad, y los pequeños retrasos al abrir una pestaña nueva, escribir o usar autocompletado se vuelven ineficientes cuando se acumulan cientos de veces al día
- Se optimizó para que un shell interactivo completamente cargado, incluso con autocompletado, resaltado de sintaxis, autosugerencias,
fzfydirenv, inicie en unos 30 milisegundos, y para que las pestañas nuevas se abran al instante - El mayor secreto es no usar frameworks ni gestores de plugins como oh-my-zsh o prezto; en su lugar, solo se hace
git clonemanual de 3 plugins y se cargan consourcedesde.zshrc - Con caché de
compinit, carga diferida (lazy-loading), prompt asíncrono y una terminal con aceleración por GPU, se minimiza tanto el tiempo de arranque como la latencia del prompt y de entrada - La mayoría de las optimizaciones no consisten en agregar cosas, sino en quitar lo innecesario; la clave es la disciplina de añadir intencionalmente solo lo que realmente usas
Por qué hace falta una terminal rápida
- Casi todo el trabajo se hace dentro de la terminal, usando todo el día Git,
kubectl,tmux, conexionesssha servidores y más - Justamente por ser una herramienta tan frecuente, tiene que ser rápida, y la latencia al abrir una pestaña nueva, escribir caracteres o usar autocompletado con Tab se siente cientos de veces al día
- Cuando esos pequeños retrasos se acumulan, es como una muerte por mil cortes (death by a thousand cuts)
Resultados al medir la velocidad de arranque del shell
- Tras las mejoras, el shell inicia en unos 30 milisegundos, medido con el comando
for i in {1..5}; do /usr/bin/time zsh -i -c exit; done - Un shell interactivo completo, con autocompletado, resaltado de sintaxis, autosugerencias,
fzfydirenv, carga en menos tiempo que un solo frame a 30fps - No fue el resultado de un gran proyecto de optimización, sino de un hábito mantenido durante años: conservar el shell minimalista y rápido
- Toda la configuración está publicada en el repositorio de dotfiles
Sin frameworks
- La mayor ganancia viene de lo que no está: no se usan oh-my-zsh, prezto ni gestores de plugins
- Si de los cientos de plugins y temas de oh-my-zsh solo usas alrededor del 5%, entonces cada vez que abres el shell terminas pagando en tiempo y recursos de cómputo por el 95% restante
- Los gestores de plugins agregan todavía más sobrecarga encima de eso
- Se usan exactamente 3 plugins, que el script de instalación clona una sola vez con git y luego carga con
sourcedesde.zshrcfzf-tab,zsh-autosuggestions,zsh-syntax-highlighting- No hay ningún gestor de plugins resolviendo dependencias al iniciar, y hacer
sourcede archivos que ya están en disco prácticamente no cuesta nada
Caché de autocompletado
compinitsuele ser una de las operaciones más costosas en un.zshrctípico, porque por defecto hace una auditoría de seguridad de todos los archivos de autocompletado cada vez que abres el shell- La solución es ejecutar el proceso completo solo si la caché (
.zcompdump) tiene más de 24 horas; en los demás casos, se omite la revisión con-C- El calificador glob
#qNmh-24significa “existe y fue modificado dentro de las últimas 24 horas” - Así,
compinitcompleto solo corre una vez al día, y el resto del tiempo se usa lectura desde caché
- El calificador glob
Carga diferida (Lazy-loading)
nvmes uno de los culpables más notorios de hacer lento el arranque del shell, y si se carga consourcedesde el inicio puede añadir fácilmente 0.5 segundos- No todos los shells necesitan
nvm; solo hace falta cuando escribesnvm, así que se envuelve en una función que se reemplaza a sí misma en el primer uso- En la primera llamada a
nvm, el stub se elimina, se hacesourcedelnvmreal (con--no-usepara evitar también resolver la versión de Node) y luego se pasan los argumentos
- En la primera llamada a
- El autocompletado de
kubectlse maneja igual: como genera el script de autocompletado llamando al binariokubectl, solo se carga después de ejecutarlo por primera vez - Cualquier herramienta que te diga que pongas
eval "$(tool init zsh)"en.zshrces candidata a carga diferida, porque al iniciar hace fork de un proceso y evalúa su salida direnvyfzfson rápidos y se usan con frecuencia, así que se mantienen con carga inmediata; hay que ser estricto al decidir qué es lo que realmente usas seguido
Prompt no bloqueante
- Un prompt que ejecuta
git statusde forma síncrona introduce retrasos en repositorios medianos o grandes, y eso se siente cada vez que presionas Enter; puede ser peor que un arranque lento - Se usa pure, que renderiza el prompt de inmediato y completa la información de Git de forma asíncrona cuando está lista
- Hubo un intento breve de reemplazarlo con
vcs_infointegrado en zsh, pero el comportamiento asíncrono de pure resultó mejor - También se podría implementar a mano un
git statusasíncrono en el prompt, pero pure ya lo envuelve muy bien para ese caso de uso
El emulador de terminal en sí
- El arranque del shell es solo la mitad de la historia; el emulador también puede añadir latencia de entrada
- Se usa Ghostty, una terminal nativa con aceleración por GPU, con una configuración de solo 7 líneas
- Combinado con el alias
tparatmux new -A -s main, una nueva ventana de terminal te regresa de inmediato a la sesión existente
Cómo medir el rendimiento de tu propio shell
- Puedes medir directamente en tu terminal dónde se va el tiempo, y hay tres tipos de latencia que revisar: tiempo de arranque, latencia del prompt y latencia de entrada
- La medición básica es ejecutar
time zsh -i -c exitvarias veces; la primera ejecución siempre será más lenta por la caché fría- Menos de 100ms está bien, menos de 50ms es excelente, y más de 500ms indica que hay cosas por corregir
- Para estadísticas más precisas, se usa hyperfine:
hyperfine --warmup 3 'zsh -i -c exit' - Aprovechar el profiler integrado en zsh
- Si pones
zmodload zsh/zprofal principio de.zshrcyzprofal final, se imprime una tabla ordenada mostrando en qué se fue el tiempo - Los primeros lugares suelen ser
compinit, hacersourcedenvm.shyeval "$(...)"; conviene corregir empezando por arriba y repetir la medición - Al terminar, se eliminan esas dos líneas
- Si pones
- Si
zprofno alcanza, se puede rastrear todo el arranque con marcas de tiempo:zsh -ixc exit 2>&1 | ts -i '%.s' | sort -rn | head -20- O configurar
PS4='+%D{%s.%6.}: 'y luego ejecutarzsh -ixc exit 2> startup.logpara revisar los saltos grandes entre líneas
- O configurar
- A veces el arranque es rápido pero el redraw del prompt es lento; si haces
cdal repositorio Git más grande que tengas y al presionar Enter notas retraso antes de ver el siguiente prompt, eso significa que el prompt está haciendo trabajo síncrono- En ese caso, puedes cambiar a un prompt asíncrono o quitar funciones relacionadas con Git
Cierre
- La mayoría de las optimizaciones tienen que ver con quitar cosas, y la clave es actuar con intención y agregar solo lo que realmente vas a usar
- Así, las decenas de sesiones que abres cada día se abren al instante, y la terminal deja de sentirse como una aplicación que te hace esperar para convertirse en una extensión de tu mente
- Cuando se trata de una herramienta que usas todo el día, esta velocidad no es negociable
- Toda la configuración anterior está publicada en el repositorio de dotfiles
1 comentarios
Comentarios de Lobste.rs
Estrictamente hablando, la mayoría de las veces se refieren al shell, no al terminal
Es mejor usar una herramienta con buenas opciones por defecto, así que solo hay que usar fish
Me gustó que traiga por defecto cosas como un autocompletado moderno donde puedes elegir con las teclas de flecha, y en mis equipos personales todavía uso ZSH, pero solo porque no he tenido tiempo de ajustar mi configuración de Nix y home manager
Estaría bien un shell con opciones razonables por defecto y autocompletado integrado rápido, sin tener que abandonar o reescribir herramientas basadas en bash
A veces me pregunto si de verdad valen la pena cosas como un prompt no bloqueante o terminales basadas en OpenGL, en vez de simplemente usar algo como
PS1="\W: "en xtermEncima es muy rápido y tiene la ventaja de ser el “estándar”, así que los bugs que le quedan por lo general son menores o los programas que corren dentro de él probablemente se consideren funcionamiento normal
Así que volví a usar xterm
El arranque de zsh en realidad es muy rápido, y solo se vuelve lento cuando el usuario lo hace lento
Basta con no meterle un montón de cosas que no entiendes, y eso también incluye bibliotecas que se llaman “minimalistas” pero ejecutan cientos de comandos cada vez que construyen el prompt
Mi configuración de zsh son unas cuantas centenas de líneas que han evolucionado muy lentamente desde los 90, y entiendo cada línea y sé por qué está ahí
Nunca intenté hacerla especialmente rápida, pero igual arranca en 20 ms, y si hago algún cambio tonto que pueda volverla lenta, lo noto enseguida y lo arreglo
Me molesta que todavía se usen tanto benchmarks rotos como
time zsh -i -c exitMiden algo totalmente equivocado, y algunos administradores de plugins de zsh incluso optimizaron para esa métrica inútil a costa de empeorar la latencia real de arranque del shell
En zsh-bench hay una sección que explica por qué este benchmark no tiene sentido: https://github.com/romkatv/zsh-bench#how-not-to-benchmark
Métricas como la latencia hasta el primer prompt o la latencia de entrada, que sí mide zsh-bench, son mucho más útiles
Pensé que iba a ser sobre bugs de terminales con aceleración por GPU, así que me alegró que no fuera eso
El cacheo de autocompletado es un buen tip, y uso zsh en una Mac del trabajo donde con solo pensar en abrir una pestaña nueva ya sale la pelotita de playa, así que ojalá ayude
En el caso del autocompletado de kubectl, me pregunto si la parte lenta es generar el autocompletado o cargarlo, y si es lo primero, me pregunto si guardarlo en un archivo y luego cargarlo reduciría el tiempo de arranque
En
jjhacen eso, y al pasarme ajjdejé de usar un prompt que ejecutabagit statusMe hubiera gustado que el autor mostrara también sus tiempos, para saber si mis 0.287 segundos son promedio o lentos
Después de medir, una
.bashrccasi vacía tarda 0.007 s, después de los keybindings de skim 0.043 s, después de mise 0.115 s, después del autocompletado de jj 0.186 s, y si además leo/etc/bashrcllega a 0.294 s, así que parece que sí hay margen de mejoratime shell -c exitel mío da unos 50 msLo que más me molesta al usar entornos Linux de otras personas son las animaciones inútiles por todos lados
En mi computadora, si presiono un atajo, la ventana del terminal se abre casi al instante, y a veces solo se ve un pequeño parpadeo entre la ventana y el prompt
Por eso importa una prueba integral de abrir una ventana nueva, hacer algo en el shell y luego cerrarla; con
time mytermy cerrando la ventana con Ctrl+D siempre estaba por debajo de 0.120 sSi eliminas animaciones inútiles y composición, se pueden lograr muchas cosas, y hasta al comparar dos hojas de cálculo vi que la diferencia saltaba de inmediato maximizando dos ventanas y alternando rápido con un atajo para enrollarlas
Hacer lo mismo en Windows con las animaciones de Excel es demasiado distractor
Incluso con una configuración vacía,
zsh -i -c exitpromedia 129.8 ms, y con toda mi configuración tarda unos 250 ms, más o menos lo mismoCon cacheo de compinit sí bajé unos 5 ms en promedio, pero como puede faltar autocompletado, no creo que valga demasiado la pena el esfuerzo
Últimamente el arranque de zsh se volvió tan lento que parecía casi colgado, y aunque no encontré la causa exacta, sí confirmé que compinit ocupaba la mayor parte de la ruta crítica
Implementé cacheo casi igual a como se propone en el artículo y eliminé la lentitud, y al ver ese elegante glob qualifier sentí que debería mejorar también mi método
Ni siquiera sabía que algo así fuera posible, y la verdad se ve como una función un poco sospechosa, pero igual la voy a usar
Antes usaba el método relativamente tosco de
date -Idpara crear la ruta destinoMe gustan las herramientas que se configuran con un lenguaje de programación completo como zsh, porque así puedes implementar esto tú mismo sin esperar a que el autor agregue la función de cacheo
Llevo casi 20 años usando zsh y nunca he usado frameworks ni administradores de plugins; da la impresión de que se usan principalmente por estilo
Yo tengo la suerte de no preocuparme por la estética de mi entorno de cómputo, y el prompt que hice yo mismo también es básico, pequeño e informativo, pero nada llamativo, y uso el tema predeterminado del terminal con fondo negro
Varias instancias del shell pueden estar haciendo lo mismo en paralelo, y me pasaba seguido al levantar instancias paralelas para práctica dentro de tmux
Además puedes compartir tu directorio home entre varios hosts, sobre todo contenedores, así que al final lo resolví con un método que incluye archivo de bloqueo, verificación de expiración y manejo condicional de
zcompilePor desgracia, parece que mi configuración de fish también fue derivando poco a poco en la misma dirección, y pienso perfilarla en un rato libre el lunes para ver si las técnicas de carga diferida realmente me sirven
Sospecho que la mayor parte de la lentitud viene del módulo git de Starship, pero también hay bastantes alias y funciones auxiliares que podrían cargarse de forma diferida
En Emacs, desde hace mucho inicializan por adelantado un shell de staging en segundo plano
Abrir un terminal significa abrir una nueva ventana con ese búfer y renombrarlo, y luego hacer fork de un hilo que vuelve a preparar el shell para la próxima vez
Así que no hay latencia de arranque
Recuerdo que hace tiempo traté de forzar una solución fuera de Emacs usando reptyr, pero al final no seguí usándola y ya no recuerdo bien por qué
https://github.com/nelhage/reptyr
Revisando algo parecido, descubrí que
zsh-abbrse come unos 100 ms del arranque, pero eso me parece aceptableSí, podría recortar 10 ms aquí y allá, pero pensando en las funciones que perdería, no me parece que valga la pena
Viviré con un tiempo de arranque de unos 300 ms; es suficientemente rápido, y además casi nunca abro terminales una tras otra ni necesito escribir inmediatamente
Aun así, el artículo estuvo bueno, conocí
hyperfiney terminé revisando algunos archivos de arranque de zshGracias a esto por fin hice esa modificación del zshrc que venía posponiendo desde hace mucho, y ahora bajó hasta 80 ms, así que quedó excelente
Mi vida sí es lo bastante larga como para tolerar terminales lentos, y a veces hasta desearía que el terminal fuera más lento
Por ejemplo, si en una consola root hubiera un retraso predeterminado de 5 segundos antes de ejecutar de verdad, para dar tiempo de cancelar un error con Ctrl+C, quizá me habría ahorrado varios días en mi juventud rebelde