La vie est trop courte pour utiliser un terminal lent
(mijndertstuij.nl)- La vitesse du terminal utilisé toute la journée influence directement l’efficacité du travail, et les micro-latences à l’ouverture d’un nouvel onglet, à la frappe ou à l’autocomplétion deviennent inefficaces lorsqu’elles s’accumulent des centaines de fois par jour
- Un shell interactif complètement chargé, avec autocomplétion, coloration syntaxique, autosuggestions, fzf et direnv, démarre désormais en environ 30 millisecondes, et les nouveaux onglets s’ouvrent instantanément
- Le plus grand secret est de ne pas utiliser de framework ni de gestionnaire de plugins comme oh-my-zsh ou prezto, mais de simplement
git clonetrois plugins puis lessourcedans le.zshrc - Le cache de
compinit, le lazy-loading, un prompt asynchrone et un terminal accéléré par GPU permettent de minimiser à la fois la latence au démarrage, celle du prompt et celle de la saisie - La plupart des optimisations ne consistent pas à ajouter quelque chose, mais à retirer l’inutile, avec comme principe clé la discipline de n’ajouter intentionnellement que ce qui est réellement utilisé
Pourquoi un terminal rapide est nécessaire
- Presque tout le travail se fait dans le terminal, avec Git,
kubectl,tmux, les connexions SSH aux serveurs, etc., utilisés toute la journée - Les outils aussi fréquemment utilisés doivent donc être rapides, et la latence à l’ouverture d’un nouvel onglet, à la saisie de caractères ou à l’autocomplétion au Tab se ressent des centaines de fois par jour
- L’accumulation de ces micro-latences ressemble à une mort par mille coupures
Résultats de mesure de la vitesse de démarrage du shell
- Après optimisation, le shell démarre en environ 30 millisecondes, mesuré avec la commande
for i in {1..5}; do /usr/bin/time zsh -i -c exit; done - Un shell interactif complet incluant autocomplétion, coloration syntaxique, autosuggestions, fzf et direnv se charge en moins de temps qu’une seule frame à 30 fps
- Ce n’est pas le résultat d’un grand projet d’optimisation, mais d’une habitude prise au fil des années pour garder le shell minimal et rapide
- Toute la configuration est publiée dans le dépôt dotfiles
Sans framework
- Le plus gros gain vient de ce qui n’existe pas : aucun usage de oh-my-zsh, prezto ou de gestionnaire de plugins
- Lorsqu’on n’utilise qu’environ 5 % des centaines de plugins et thèmes de oh-my-zsh, on paie malgré tout, à chaque ouverture du shell, le coût en temps et en ressources de calcul des 95 % restants
- Un gestionnaire de plugins ajoute encore de l’overhead par-dessus
- Exactement 3 plugins sont utilisés, clonés une fois par le script d’installation puis
sourcedans le.zshrcfzf-tab,zsh-autosuggestions,zsh-syntax-highlighting- Il n’y a pas de gestionnaire de plugins qui résout les dépendances au démarrage, et
sourcedes fichiers déjà présents sur le disque ne coûte pratiquement rien
Cache de l’autocomplétion
compinitest l’une des opérations les plus coûteuses dans un.zshrcclassique, car il effectue par défaut un audit de sécurité de tous les fichiers d’autocomplétion à chaque ouverture du shell- La solution consiste à n’exécuter le lancement complet que si le cache (
.zcompdump) a plus de 24 heures, et à utiliser sinon-Cpour sauter la vérification- Le qualificateur glob
#qNmh-24signifie « existe et a été modifié dans les dernières 24 heures » - Un
compinitcomplet n’est donc exécuté qu’une fois par jour, le reste du temps on utilise la lecture en cache
- Le qualificateur glob
Chargement différé (Lazy-loading)
- nvm est l’un des coupables les plus notoires du ralentissement au démarrage du shell : le charger immédiatement au lancement peut facilement ajouter 0,5 seconde
- nvm n’est pas nécessaire dans tous les shells, seulement quand on tape
nvm, donc il est encapsulé dans une fonction qui se remplace elle-même lors de sa première utilisation- Le premier appel à
nvmsupprime le stub, charge le vrai nvm (avec--no-usepour éviter aussi la résolution de version de node) puis transmet les arguments
- Le premier appel à
- L’autocomplétion de
kubectlest gérée de la même façon : comme le script d’autocomplétion est généré en appelant le binairekubectl, il n’est chargé qu’après la première exécution réelle - Tous les outils qui recommandent d’ajouter
eval "$(tool init zsh)"dans le.zshrcsont des candidats au lazy-loading, car ils forkent un processus au démarrage puis évaluent sa sortie direnvetfzfsont gardés en chargement immédiat parce qu’ils sont rapides et souvent utilisés ; il faut juger avec rigueur ce qui sert réellement souvent
Prompt non bloquant
- Un prompt qui exécute
git statusde manière synchrone introduit de la latence dans les dépôts un peu volumineux, et cela peut être pire qu’un démarrage lent puisque c’est perceptible à chaque pression sur Entrée - Le prompt pure est utilisé : il s’affiche immédiatement puis remplit les informations Git de manière asynchrone lorsqu’elles sont prêtes
- Un remplacement par
vcs_info, intégré à zsh, a été brièvement essayé, mais le comportement asynchrone de pure était meilleur - Il serait aussi possible d’implémenter directement un
git statusasynchrone dans le prompt, mais pure encapsule déjà très bien ce cas d’usage
L’émulateur de terminal lui-même
- Le démarrage du shell ne raconte que la moitié de l’histoire : l’émulateur lui-même ajoute aussi de la latence de saisie
- Ghostty, un terminal natif accéléré par GPU, est utilisé, avec seulement 7 lignes de configuration
- Combiné à l’alias
tpourtmux new -A -s main, une nouvelle fenêtre de terminal ramène immédiatement à la session existante
Comment mesurer les performances de son shell
- Il est possible de mesurer directement dans le terminal où le temps est dépensé ; les trois latences à vérifier sont le temps de démarrage, la latence du prompt et la latence de saisie
- La mesure de base consiste à exécuter
time zsh -i -c exitplusieurs fois ; la première exécution est toujours plus lente à cause du cache froid- En dessous de 100 ms, c’est correct ; sous 50 ms, c’est excellent ; au-delà de 500 ms, il y a probablement quelque chose à corriger
- Pour des statistiques précises, utiliser hyperfine :
hyperfine --warmup 3 'zsh -i -c exit' - Utiliser aussi le profileur intégré à zsh
- Ajouter
zmodload zsh/zproftout en haut du.zshrc, puiszproftout en bas, pour afficher un tableau trié de l’endroit où le temps est consommé - Les éléments en tête sont souvent
compinit, lesourcedenvm.shoueval "$(...)"; il faut corriger à partir du haut de la liste puis relancer en boucle - Une fois terminé, supprimer ces deux lignes
- Ajouter
- Si
zprofne suffit pas, on peut suivre tout le démarrage avec des timestamps :zsh -ixc exit 2>&1 | ts -i '%.s' | sort -rn | head -20- Ou définir
PS4='+%D{%s.%6.}: 'puis lancerzsh -ixc exit 2> startup.logafin d’identifier les grands sauts entre les lignes
- Ou définir
- Le démarrage peut être rapide alors que le redraw du prompt reste lent : il suffit de faire
cddans le plus gros dépôt Git, puis d’appuyer sur Entrée ; s’il y a un délai avant l’apparition du prompt suivant, c’est que le prompt effectue du travail synchrone- Les options sont alors de passer à un prompt asynchrone ou de retirer les fonctionnalités Git
Conclusion
- La plupart des optimisations consistent à retirer des choses ; la clé est d’agir intentionnellement et de n’ajouter que ce qui sera réellement utilisé
- Ainsi, les dizaines de sessions ouvertes chaque jour s’ouvrent toutes instantanément, et le terminal cesse d’être une application qui fait attendre pour devenir un prolongement de l’esprit
- Pour un outil utilisé toute la journée, cette vitesse n’est pas négociable
- Toute la configuration ci-dessus est publiée dans le dépôt dotfiles
1 commentaires
Avis sur Lobste.rs
Techniquement, dans la plupart des cas, on parle non pas du terminal, mais du shell
Mieux vaut utiliser un outil avec de bons réglages par défaut, donc il suffit d’utiliser fish
J’ai apprécié qu’il propose par défaut des fonctionnalités modernes comme une complétion par tabulation que l’on peut parcourir avec les touches fléchées ; sur ma machine personnelle, j’utilise encore ZSH, mais seulement parce que je n’ai pas eu le temps d’ajuster ma configuration Nix et home manager
Un shell avec des valeurs par défaut raisonnables et une complétion intégrée rapide, sans devoir abandonner ou réécrire les outils basés sur bash
Je me demande parfois si des choses comme les prompts non bloquants ou les terminaux basés sur OpenGL valent vraiment mieux qu’un xterm avec juste
PS1="\W: "En plus, il est très rapide et a l’avantage d’être le « standard », donc les bugs qui restent sont en général mineurs ou concernent des programmes qui risquent de considérer ce comportement comme normal
Du coup, je suis revenu à xterm
Le démarrage de zsh est à l’origine très rapide, et il ne devient lent que si l’utilisateur le rend lent
Il suffit de ne pas y empiler des tas de choses qu’on ne comprend pas, y compris des bibliothèques qui se disent « minimalistes » tout en exécutant des centaines de commandes à chaque construction du prompt
Ma configuration zsh fait quelques centaines de lignes, a évolué très lentement depuis les années 90, et je comprends chaque ligne ainsi que la raison de sa présence
Je n’ai jamais cherché spécialement à la rendre rapide, mais elle démarre toujours en 20 ms, et si je fais une modification stupide susceptible de la ralentir, je m’en rends compte tout de suite et je peux la corriger
Je n’aime pas que des benchmarks cassés comme
time zsh -i -c exitsoient encore couramment utilisésIls mesurent complètement la mauvaise chose, et certains gestionnaires de plugins zsh se sont optimisés pour cette métrique inutile au prix de la latence réelle au démarrage du shell
zsh-bench a une section qui explique pourquoi ce benchmark n’a aucun sens : https://github.com/romkatv/zsh-bench#how-not-to-benchmark
Des métriques comme la latence jusqu’au premier prompt ou la latence de saisie, que mesure zsh-bench, sont bien plus utiles
Je croyais qu’on allait parler de bugs de terminaux accélérés par GPU, donc j’étais content que ce ne soit pas le cas
Le cache de complétion est un bon conseil, et j’utilise zsh sur un Mac d’entreprise où il suffit de penser à ouvrir un nouvel onglet pour voir apparaître la roue colorée, donc j’espère que ça aidera
Pour la complétion kubectl, je me demande si la lenteur vient de la génération de la complétion ou de son chargement ; si c’est la première, je me demande si l’enregistrer dans un fichier puis la relire réduirait le temps de démarrage
C’est ce qu’ils font dans
jj, et en passant àjj, j’ai abandonné les prompts qui exécutentgit statusJ’aurais aimé que l’auteur montre aussi ses propres temps, pour savoir si mes 0,287 s sont dans la moyenne ou plutôt lents
Après avoir mesuré ensuite, un
.bashrcpresque vide prend 0,007 s, après les raccourcis clavier skim 0,043 s, après mise 0,115 s, après la complétion jj 0,186 s, et avec/etc/bashrcchargé aussi 0,294 s, donc il semble y avoir de la marge d’améliorationtime shell -c exit, le mien tourne autour de 50 msCe qui m’agace le plus quand j’utilise l’environnement Linux de quelqu’un d’autre, ce sont toutes ces animations inutiles partout
Sur mon ordinateur, quand j’appuie sur un raccourci, la fenêtre de terminal s’ouvre presque instantanément, et parfois on ne voit qu’un très bref clignotement entre la fenêtre et le prompt
C’est pourquoi un test complet de bout en bout, consistant à ouvrir une nouvelle fenêtre, faire quelque chose dans le shell puis la refermer, est important ; en faisant
time mytermpuis Ctrl+D dans la fenêtre pour la fermer, j’étais toujours sous les 0,120 sQuand on supprime les animations et la composition inutiles, beaucoup de choses deviennent possibles ; même pour comparer deux feuilles de calcul, je maximisais deux fenêtres puis je basculais rapidement entre elles avec un raccourci d’enroulement de fenêtre, et la différence sautait immédiatement aux yeux
Faire la même chose sous Windows avec les animations d’Excel est beaucoup trop distrayant
Même avec une configuration vide,
zsh -i -c exitfait en moyenne 129,8 ms, et avec ma configuration complète, c’est à peu près 250 ms, donc dans le même ordre de grandeurLe cache compinit m’a bien fait gagner environ 5 ms en moyenne, mais comme certaines complétions peuvent manquer, je ne pense pas que l’effort en vaille vraiment la peine
Récemment, le démarrage de zsh est devenu presque bloqué tant il était lent, et même si je n’ai pas identifié précisément la cause, j’ai confirmé que compinit occupait la majeure partie du chemin critique
J’ai mis en place un cache presque identique à la méthode proposée dans l’article, ce qui a supprimé le ralentissement, et en voyant ce joli glob qualifier, je me suis dit que je devrais améliorer ma propre méthode
Je ne savais même pas que ce genre de fonctionnalité existait, et pour être honnête ça a même un petit côté suspect, mais je vais quand même l’utiliser
Avant cela, j’utilisais une approche assez grossière avec
date -Idpour créer le chemin cibleJ’aime les outils configurés comme de véritables langages de programmation, comme zsh, où l’on peut implémenter soi-même ce genre de cache sans attendre que l’auteur ajoute la fonctionnalité
J’utilise zsh depuis presque 20 ans sans jamais avoir recours à un framework ni à un gestionnaire de plugins, et j’ai l’impression que ces outils servent surtout au style
J’ai la chance de ne pas me soucier de l’esthétique de mon environnement informatique, et mon prompt maison est lui aussi basique, compact et informatif, mais pas du tout tape-à-l’œil, avec le thème de terminal par défaut sur fond noir
Plusieurs instances de shell peuvent aussi faire la même chose en parallèle, ce qui m’est souvent arrivé en lançant des instances parallèles de test dans tmux
On peut aussi partager son répertoire personnel entre plusieurs hôtes, en particulier entre des conteneurs ; j’ai donc fini par mettre de l’ordre là-dedans avec une méthode comprenant fichier de verrouillage, vérification d’expiration et gestion conditionnelle de
zcompileMalheureusement, la configuration fish semble elle aussi avoir lentement dérivé dans la même direction ; je vais donc faire du profiling pendant une pause lundi pour vérifier si les techniques de chargement différé sont réellement utiles dans mon cas
Je soupçonne que la majeure partie du temps perdu vient du module git de Starship, mais j’ai aussi pas mal d’alias et de fonctions utilitaires qui pourraient être chargés à la demande
Dans Emacs, on préinitialise depuis longtemps un shell de staging en arrière-plan
Ouvrir un terminal consiste à ouvrir une nouvelle fenêtre sur ce buffer et à le renommer, puis à forker un thread qui prépare à nouveau le shell pour la prochaine fois
Il n’y a donc aucune latence au démarrage
Je me souviens avoir autrefois essayé de bricoler une solution hors d’Emacs avec reptyr, mais au final je n’ai pas continué à l’utiliser, sans bien me rappeler pourquoi
https://github.com/nelhage/reptyr
En fouillant un peu de la même manière, j’ai découvert que
zsh-abbrme coûtait environ 100 ms au démarrage, mais ça me vaJe pourrais sûrement gratter 10 ms par-ci par-là, mais vu les fonctionnalités perdues, ça n’en vaut pas la peine
Je vais vivre avec un temps de démarrage d’environ 300 ms ; c’est largement assez rapide, et j’ouvre rarement des terminaux à la chaîne ou avec la nécessité de taper immédiatement
Cela dit, l’article était bon, j’ai découvert
hyperfine, et ça m’a poussé à regarder quelques fichiers de démarrage zshGrâce à ça, j’ai enfin modifié mon zshrc, que je repoussais depuis longtemps, et je suis maintenant descendu à 80 ms, ce qui est excellent
Ma vie est assez longue pour supporter un terminal lent, et parfois j’aimerais même qu’un terminal soit plus lent
Par exemple, si une console root imposait par défaut un délai de 5 secondes avant l’exécution réelle, pour laisser le temps d’annuler une faute de frappe avec Ctrl+C, ça m’aurait peut-être évité de perdre quelques jours pendant ma jeunesse rebelle