5 puntos por GN⁺ 4 시간 전 | 1 comentarios | Compartir por WhatsApp
  • 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, fzf y direnv, 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 clone manual de 3 plugins y se cargan con source desde .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, conexiones ssh a 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, fzf y direnv, 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 source desde .zshrc
    • fzf-tab, zsh-autosuggestions, zsh-syntax-highlighting
    • No hay ningún gestor de plugins resolviendo dependencias al iniciar, y hacer source de archivos que ya están en disco prácticamente no cuesta nada

Caché de autocompletado

  • compinit suele ser una de las operaciones más costosas en un .zshrc tí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-24 significa “existe y fue modificado dentro de las últimas 24 horas”
    • Así, compinit completo solo corre una vez al día, y el resto del tiempo se usa lectura desde caché
    Publicidad

Carga diferida (Lazy-loading)

  • nvm es uno de los culpables más notorios de hacer lento el arranque del shell, y si se carga con source desde el inicio puede añadir fácilmente 0.5 segundos
  • No todos los shells necesitan nvm; solo hace falta cuando escribes nvm, 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 hace source del nvm real (con --no-use para evitar también resolver la versión de Node) y luego se pasan los argumentos
  • El autocompletado de kubectl se maneja igual: como genera el script de autocompletado llamando al binario kubectl, solo se carga después de ejecutarlo por primera vez
  • Cualquier herramienta que te diga que pongas eval "$(tool init zsh)" en .zshrc es candidata a carga diferida, porque al iniciar hace fork de un proceso y evalúa su salida
  • direnv y fzf son 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 status de 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_info integrado en zsh, pero el comportamiento asíncrono de pure resultó mejor
  • También se podría implementar a mano un git status así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 t para tmux new -A -s main, una nueva ventana de terminal te regresa de inmediato a la sesión existente
Publicidad

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 exit varias 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/zprof al principio de .zshrc y zprof al final, se imprime una tabla ordenada mostrando en qué se fue el tiempo
    • Los primeros lugares suelen ser compinit, hacer source de nvm.sh y eval "$(...)"; conviene corregir empezando por arriba y repetir la medición
    • Al terminar, se eliminan esas dos líneas
  • Si zprof no 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 ejecutar zsh -ixc exit 2> startup.log para revisar los saltos grandes entre líneas
  • A veces el arranque es rápido pero el redraw del prompt es lento; si haces cd al 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

 
GN⁺ 4 시간 전
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

    • El ZSH de mi trabajo se volvió ridículamente lento desde hace como un año, así que probé fish y de verdad me encantaron sus funciones de calidad de vida
      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
    • Ojalá alguien hiciera un fish compatible con bash
      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
    • La vida es demasiado corta para instalar herramientas nuevas; con que tenga valores por defecto razonables basta
  • 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 xterm

    • Durante años evité usar xterm a propósito, pero al revisar varios emuladores de terminal me sorprendió bastante ver que xterm soporta fuentes OpenType, UTF-8, la mayoría de los emoji, color de 24 bits y además usa poca memoria
      Encima 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
    • No lo vale tanto
      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 exit
    Miden 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 jj hacen eso, y al pasarme a jj dejé de usar un prompt que ejecutaba git status
    Me 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 .bashrc casi 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/bashrc llega a 0.294 s, así que parece que sí hay margen de mejora

    • En el artículo decían que el shell mismo estaba en 30 ms, y en la misma prueba de time shell -c exit el mío da unos 50 ms
      Lo 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 myterm y cerrando la ventana con Ctrl+D siempre estaba por debajo de 0.120 s
      Si 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
    • Menos de 100 ms se ve difícil en mi entorno
      Incluso con una configuración vacía, zsh -i -c exit promedia 129.8 ms, y con toda mi configuración tarda unos 250 ms, más o menos lo mismo
      Con 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 -Id para crear la ruta destino
    Me 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

    • El cacheo de compinit es frustrante porque el caché puede quedarse viejo
      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 zcompile
    • El tiempo de carga de ZSH se puso tan mal que simplemente probé fish
      Por 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

    • Me gusta; se siente como el proceso zygote de Android
  • Revisando algo parecido, descubrí que zsh-abbr se come unos 100 ms del arranque, pero eso me parece aceptable
    Sí, 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í hyperfine y terminé revisando algunos archivos de arranque de zsh

  • Gracias 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