A vida é curta demais para usar um terminal lento
(mijndertstuij.nl)- A velocidade do terminal que você usa o dia inteiro influencia diretamente sua eficiência, e pequenos atrasos ao abrir uma nova aba, digitar ou usar autocompletar se acumulam centenas de vezes por dia e viram ineficiência
- Um shell interativo totalmente carregado, incluindo autocompletar, destaque de sintaxe, sugestões automáticas, fzf e direnv, passa a iniciar em cerca de 30 milissegundos, e novas abas passam a abrir instantaneamente
- O maior segredo é não usar frameworks nem gerenciadores de plugins como oh-my-zsh ou prezto; em vez disso, apenas 3 plugins são clonados diretamente com git e carregados via
sourceno.zshrc - Com cache do
compinit, lazy-loading, prompt assíncrono e terminal com aceleração por GPU, os atrasos de inicialização, prompt e entrada são todos minimizados - A maior parte das otimizações não vem de adicionar coisas, mas de remover o desnecessário; a chave é a disciplina de acrescentar só o que você realmente usa
Por que um terminal rápido é necessário
- Quase todo o trabalho acontece dentro do terminal, com uso constante de Git, kubectl, tmux e conexões ssh para servidores ao longo do dia
- Ferramentas usadas com tanta frequência precisam ser rápidas, e o atraso ao abrir novas abas, digitar caracteres e usar autocompletar é sentido centenas de vezes por dia
- Esse acúmulo de pequenos atrasos é como morrer por mil cortes (death by a thousand cuts)
Resultado da medição da velocidade de inicialização do shell
- Após as melhorias, o shell inicia em cerca de 30 milissegundos, medido com o comando
for i in {1..5}; do /usr/bin/time zsh -i -c exit; done - Um shell interativo completo com autocompletar, destaque de sintaxe, sugestões automáticas, fzf e direnv carrega em menos tempo do que um único frame a 30 fps
- Isso não foi resultado de um grande projeto de otimização, mas de um hábito mantido por anos de deixar o shell mínimo e rápido
- Todas as configurações estão públicas no repositório de dotfiles
Sem framework
- O maior ganho vem justamente daquilo que não existe: não usar oh-my-zsh, prezto nem gerenciadores de plugins
- Mesmo usando só cerca de 5% dos centenas de plugins e temas do oh-my-zsh, você acaba pagando o custo de tempo e recursos computacionais dos 95% restantes toda vez que abre o shell
- Gerenciadores de plugins ainda adicionam overhead extra por cima disso
- São usados exatamente 3 plugins, clonados uma única vez pelo script de instalação e carregados com
sourceno.zshrcfzf-tab,zsh-autosuggestions,zsh-syntax-highlighting- Não há gerenciador de plugins resolvendo dependências na inicialização, e dar
sourceem arquivos que já estão no disco praticamente não tem custo
Cache de autocompletar
- O
compinité uma das operações mais custosas em um.zshrccomum e, por padrão, faz uma auditoria de segurança em todos os arquivos de autocompletar toda vez que o shell é aberto - A solução é fazer a execução completa apenas quando o cache (
.zcompdump) tiver mais de 24 horas; fora isso, usar-Cpara pular a verificação- O qualificador de glob
#qNmh-24significa "existe e foi modificado nas últimas 24 horas" - Assim, o
compinitcompleto roda só uma vez por dia, e no restante do tempo usa leitura em cache
- O qualificador de glob
Lazy-loading
- O nvm é um dos vilões mais conhecidos da lentidão na inicialização do shell; se for carregado com
sourcelogo na partida, pode facilmente adicionar 0,5 segundo - Nem todo shell precisa de nvm, então ele só é necessário quando você digita
nvm; por isso, ele é envolvido em uma função que se substitui no primeiro uso- A primeira chamada de
nvmremove o stub, fazsourcedo nvm real (com--no-usepara impedir resolução de versão do node) e repassa os argumentos
- A primeira chamada de
- O autocompletar do kubectl segue a mesma ideia: como ele chama o binário
kubectlpara gerar o script de autocompletar, só é carregado depois da primeira execução real - Toda ferramenta que manda colocar
eval "$(tool init zsh)"no.zshrccria um processo na inicialização e avalia a saída, então vira candidata a lazy-loading - direnv e fzf são rápidos e usados com frequência, então continuam sendo carregados imediatamente; é preciso julgar com rigor o que você realmente usa com frequência
Prompt não bloqueante
- Um prompt que executa
git statusde forma síncrona sofre atrasos em repositórios minimamente grandes, e isso pode ser pior do que uma inicialização lenta porque é percebido toda vez que se pressiona Enter - É usado o pure, que renderiza o prompt imediatamente e preenche as informações do git de forma assíncrona quando estiverem prontas
- Houve uma breve tentativa de trocar pelo
vcs_infoembutido do zsh, mas o comportamento assíncrono do pure foi melhor - Também seria possível implementar manualmente um
git statusassíncrono no próprio prompt, mas o pure já encapsula bem esse caso de uso
O próprio emulador de terminal
- A inicialização do shell é só metade da história; o próprio emulador pode adicionar latência de entrada
- É usado o Ghostty, um terminal nativo com aceleração por GPU, com uma configuração de apenas 7 linhas
- Em conjunto com o alias
tparatmux new -A -s main, uma nova janela do terminal retorna imediatamente para a sessão existente
Como medir o desempenho do seu shell
- Dá para medir diretamente no terminal onde o tempo está sendo gasto, e há três tipos de atraso a observar: tempo de inicialização, atraso do prompt e atraso de entrada
- A medição básica é executar
time zsh -i -c exitalgumas vezes; a primeira execução sempre é mais lenta por causa do cache frio- Menos de 100 ms está ok, menos de 50 ms é excelente, e acima de 500 ms já indica que há algo para ajustar
- Para estatísticas mais precisas, use o hyperfine:
hyperfine --warmup 3 'zsh -i -c exit' - Aproveite o profiler embutido do zsh
- Coloque
zmodload zsh/zprofno topo do.zshrcezprofno fim para obter uma tabela ordenada mostrando onde o tempo foi gasto - Os itens no topo normalmente são
compinit,sourcedenvm.sheeval "$(...)"; corrija começando pelo primeiro e repita a medição - Ao terminar, remova essas duas linhas
- Coloque
- Se o zprof não for suficiente, rastreie toda a inicialização com timestamps:
zsh -ixc exit 2>&1 | ts -i '%.s' | sort -rn | head -20- Ou defina
PS4='+%D{%s.%6.}: 'e rodezsh -ixc exit 2> startup.log, depois verifique os grandes saltos entre linhas
- Ou defina
- A inicialização pode ser rápida, mas o redraw do prompt ainda ser lento; entre no maior repositório git com
cde pressione Enter — se houver atraso até o próximo prompt aparecer, então o prompt está fazendo trabalho síncrono- Nesse caso, você pode migrar para um prompt assíncrono ou remover os recursos de Git
Encerrando
- A maior parte da otimização consiste em remover coisas; o mais importante é agir com intenção e adicionar apenas o que você realmente vai usar
- Assim, todas as dezenas de sessões abertas ao longo do dia passam a surgir instantaneamente, e o terminal deixa de ser um aplicativo que faz você esperar para virar uma extensão da sua mente
- Para uma ferramenta usada o dia inteiro, essa velocidade é inegociável
- Todas as configurações acima estão públicas no repositório de dotfiles
1 comentários
Opiniões no Lobste.rs
Tecnicamente, na maioria dos casos isso quer dizer shell, não terminal
É melhor usar uma ferramenta com bons padrões, então é só usar fish
Gostei de ele já vir com coisas modernas por padrão, como autocompletar com Tab em que dá para escolher com as setas; ainda uso ZSH na minha máquina pessoal, mas só porque não tive tempo de mexer nas configurações do Nix e do home manager
Um shell com padrões sensatos e autocompletar rápido embutido, sem precisar abandonar nem reescrever ferramentas baseadas em bash
Às vezes me pergunto se coisas como prompt não bloqueante ou terminais baseados em OpenGL realmente valem mais do que usar algo como
PS1="\W: "no xtermAlém disso, é muito rápido e tem a vantagem de ser o “padrão”, então os bugs que restam em geral são pequenos ou os programas rodando nele provavelmente vão tratá-los como comportamento normal
Então voltei a usar xterm
A inicialização do zsh já é muito rápida por natureza, e só fica lenta quando o usuário a torna lenta
Basta não encher de coisas que você não entende; isso também inclui bibliotecas que se dizem “minimalistas”, mas executam centenas de comandos toda vez que montam o prompt
Minha configuração do zsh tem algumas centenas de linhas e evoluiu bem devagar desde os anos 90; entendo todas elas e sei por que estão lá
Nunca tentei deixá-la especialmente rápida, mas ainda assim inicia em 20 ms, e se eu fizer alguma mudança idiota que a deixe lenta, percebo na hora e conserto
Não gosto que benchmarks quebrados como
time zsh -i -c exitainda sejam tão usadosEles medem um alvo completamente errado, e alguns gerenciadores de plugins do zsh chegaram a otimizar para essa métrica inútil sacrificando a latência real de inicialização do shell
O zsh-bench tem uma seção explicando por que esse benchmark não faz sentido: https://github.com/romkatv/zsh-bench#how-not-to-benchmark
Métricas como latência até o primeiro prompt ou latência de entrada, que o zsh-bench mede, são muito mais úteis
Fiquei feliz por não ser uma discussão sobre bugs de terminal com aceleração por GPU
Cache de completions é uma boa dica; uso zsh no Mac do trabalho, e só de pensar em abrir uma aba nova já aparece a bolinha colorida, então espero que ajude
No caso do completion do kubectl, fico curioso se a lentidão está na geração do completion ou no carregamento; se for o primeiro caso, talvez salvar em arquivo e depois carregar reduza o tempo de inicialização
Faço isso no
jj, e ao migrar parajjabandonei prompts que executavamgit statusPena que o autor não mostrou também o próprio tempo, porque aí eu saberia se meus 0,287 s são média ou lentos
Depois medi melhor: um
.bashrcquase vazio leva 0,007 s, depois dos atalhos do skim 0,043 s, depois do mise 0,115 s, depois do completion do jj 0,186 s, e lendo até/etc/bashrcvai para 0,294 s, então parece haver espaço para melhorartime shell -c exito meu dá algo em torno de 50 msO que mais me irrita ao usar o Linux de outras pessoas são as animações inúteis espalhadas por todo lado
No meu computador, aperto o atalho e a janela do terminal abre quase instantaneamente; às vezes só vejo um piscar curto entre a janela e o prompt
Por isso, o teste de ponta a ponta completo — abrir uma nova janela, fazer algo no shell e fechar — é o que importa, e com
time mytermseguido de Ctrl+D para fechar a janela, sempre ficou abaixo de 0,120 sQuando você elimina animações inúteis e composição, muita coisa passa a ser possível; até para comparar duas planilhas, eu maximizava duas janelas e alternava rapidamente com um atalho de enrolar janelas, e a diferença aparecia na hora
Tentar fazer o mesmo no Windows com as animações do Excel é dispersivo demais
Mesmo com configuração vazia,
zsh -i -c exitdá em média 129,8 ms, e com a configuração completa fica parecido, em torno de 250 msCom cache do compinit consegui reduzir cerca de 5 ms em média, mas como completions podem ficar faltando, não acho que o esforço valha tanto a pena
Recentemente a inicialização do zsh ficou tão lenta que parecia quase travada; não descobri exatamente a causa, mas confirmei que o compinit ocupava a maior parte do caminho crítico
Implementei cache quase do mesmo jeito sugerido no texto e eliminei a lentidão, e ao ver aquele glob qualifier elegante pensei que deveria melhorar minha própria solução
Eu nem sabia que dava para fazer aquilo; sinceramente parece meio suspeito, mas ainda assim vou usar
Antes, eu usava a abordagem relativamente grosseira de
date -Idpara criar o caminho de destinoGosto de ferramentas configuradas com uma linguagem de programação completa como o zsh, porque dá para implementar esse tipo de coisa sem esperar o autor adicionar um recurso de cache
Em quase 20 anos usando zsh, nunca usei framework nem gerenciador de plugins; parece que essas coisas são usadas principalmente por estilo
Tenho a sorte de não me importar com a estética do meu ambiente computacional, e meu prompt caseiro também é básico, pequeno e informativo, mas nada chamativo; uso o tema padrão do terminal com fundo preto
Várias instâncias do shell podem acabar fazendo a mesma coisa em paralelo, e eu via isso com frequência ao subir instâncias paralelas de laboratório no tmux
Também dá para compartilhar o diretório home entre vários hosts, especialmente contêineres, então no fim organizei tudo com um esquema que inclui arquivo de lock, verificação de expiração e tratamento condicional de
zcompileInfelizmente, parece que a configuração do fish também foi escorregando aos poucos na mesma direção, e pretendo fazer profiling na folga de segunda-feira para ver se técnicas de lazy loading realmente ajudam no meu caso
A maior parte da lentidão provavelmente vem do módulo git do Starship, mas também tenho várias aliases e funções auxiliares que poderiam ser carregadas sob demanda
No Emacs, há muito tempo já deixam um shell de staging em segundo plano pré-inicializado
Abrir um terminal significa abrir uma nova janela para esse buffer e renomeá-lo, e fazer fork de uma thread que prepara outro shell para a próxima vez
Então não há latência de inicialização
Lembro que no passado tentei forçar uma solução fora do Emacs com reptyr, mas no fim não continuei usando aquilo, e já nem lembro direito o motivo
https://github.com/nelhage/reptyr
Investigando algo parecido, descobri que o
zsh-abbrconsome cerca de 100 ms do tempo de inicialização, mas isso para mim é aceitávelDá para cortar 10 ms aqui e ali, mas considerando a funcionalidade perdida, não parece valer a pena
Vou viver com cerca de 300 ms de inicialização; é rápido o bastante, e raramente fico abrindo terminais em sequência ou preciso digitar imediatamente
Ainda assim, o texto foi bom, conheci o
hyperfinee acabei olhando alguns arquivos de inicialização do zshGraças a isso, finalmente mexi no zshrc que vinha adiando havia muito tempo, e agora caiu para 80 ms, o que está ótimo
Minha vida é longa o bastante para aguentar terminais lentos, e às vezes eu até queria que o terminal fosse mais lento
Por exemplo, se no console root houvesse um atraso padrão de 5 segundos antes da execução real, para dar tempo de cancelar um erro de digitação com Ctrl+C, talvez eu tivesse economizado alguns dias na minha juventude rebelde