Обновление: один из читателей прислал обоснованные возражения по поводу того, как я измерял «быстродействие», и по паре моих рекомендаций. Я написал продолжение — о правильном бенчмаркинге оболочек и о том, в чём этот пост не дотягивает.
Практически вся моя работа происходит в терминале. Git, kubectl, tmux, SSH-подключения к серверам — всё это открыто почти целый день. Инструмент, которым пользуешься настолько много, обязан быть быстрым. Любая задержка при открытии новой вкладки, наборе символа или нажатии Tab для автодополнения ощущается сотни раз в день. Это смерть от тысячи порезов.
Моя оболочка запускается примерно за 30 миллисекунд:
$ for i in {1..5}; do /usr/bin/time zsh -i -c exit; done
0.03 real 0.02 user 0.01 sys
0.03 real 0.02 user 0.01 sys
...
Это полноценная интерактивная оболочка с автодополнениями, подсветкой синтаксиса, автоподсказками, fzf и direnv — и всё это укладывается в меньше времени, чем один кадр при 30 fps. Новая вкладка открывается мгновенно. Никакого специального проекта по оптимизации за этим не стоит: я всегда держал конфигурацию оболочки минималистичной и быстрой, и со временем это превратилось в привычку. Вот как я к этому подхожу — всё можно найти в моём репозитории dotfiles.
Никаких фреймворков
Самый большой выигрыш даёт то, чего нет: ни oh-my-zsh, ни prezto, ни менеджера плагинов. Честно говоря, я никогда не понимал привлекательности этих фреймворков. Люди устанавливают oh-my-zsh с его сотнями плагинов и тем, в итоге используют от силы 5% возможностей, а за оставшиеся 95% платят своим временем и вычислительными ресурсами при каждом открытии оболочки. Менеджеры плагинов добавляют к этому ещё собственные накладные расходы.
Я использую ровно три плагина, которые мой установочный скрипт клонирует через git один раз, а .zshrc подключает их через source:
source ~/.zsh/fzf-tab/fzf-tab.plugin.zsh
source ~/.zsh/zsh-autosuggestions/zsh-autosuggestions.zsh
source ~/.zsh/zsh-syntax-highlighting/zsh-syntax-highlighting.zsh
Никакого менеджера плагинов, который разрешал бы зависимости при запуске, — а source файла, уже лежащего на диске, обходится практически бесплатно.
Кэширование автодополнений
compinit — одна из самых затратных операций в типичном .zshrc. По умолчанию при каждом открытии оболочки она проводит аудит безопасности для каждого файла автодополнений. Решение: выполнять полный запуск только в том случае, если кэш (.zcompdump) старше 24 часов, а в остальное время пропускать проверку с флагом -C:
autoload -Uz compinit
if [[ -n ~/.zcompdump(#qNmh-24) ]]; then
compinit -C
else
compinit
fi
Квалификатор глоба (#qNmh-24) означает «файл существует и был изменён в течение последних 24 часов». Таким образом, полный compinit запускается один раз в сутки, а всё остальное время используется кэш.
Отложенная загрузка (lazy loading)
nvm — пожалуй, самый известный убийца скорости запуска оболочки: нетерпеливый (eager) source этого инструмента легко добавляет полсекунды. Но nvm нужен мне не в каждой оболочке — он нужен, когда я набираю nvm. Поэтому я оборачиваю его в функцию, которая при первом вызове заменяет саму себя:
export NVM_DIR="$HOME/.nvm"
nvm() {
unset -f nvm
[ -s "/opt/homebrew/opt/nvm/nvm.sh" ] && \. "/opt/homebrew/opt/nvm/nvm.sh" --no-use
[ -s "/opt/homebrew/opt/nvm/etc/bash_completion.d/nvm" ] && \. "/opt/homebrew/opt/nvm/etc/bash_completion.d/nvm"
nvm "$@"
}
Первый вызов nvm удаляет заглушку, подключает настоящий инструмент (с флагом --no-use, чтобы не разрешать версию Node), и передаёт аргументы дальше.
Та же идея работает для автодополнений kubectl, которые обращаются к бинарнику kubectl, чтобы сгенерировать скрипт автодополнения. Я загружаю их только после того, как впервые запускаю kubectl:
kubectl() {
command kubectl "$@"
local ret=$?
if [[ -z $KUBECTL_COMPLETE ]]; then
source <(command kubectl completion zsh)
KUBECTL_COMPLETE=1
fi
return $ret
}
Этот приём применим ко многим вещам: всё, что предлагает добавить eval "$(tool init zsh)" в .zshrc, — кандидат на отложенную загрузку, потому что каждая такая строка порождает дочерний процесс и выполняет его вывод при старте. direnv и fzf я загружаю нетерпеливо — они быстрые и используются постоянно. Будьте строги в выборе того, чем действительно пользуетесь часто.
Неблокирующее приглашение (prompt)
Приглашение, которое синхронно запускает git status, будет тормозить в любом репозитории приличного размера. Эта задержка ощущается при каждом нажатии Enter — что, пожалуй, хуже медленного запуска оболочки. Я использую pure: он немедленно отрисовывает приглашение, а информацию о git добавляет асинхронно, когда она готова. Я ненадолго пробовал заменить его встроенным vcs_info из zsh, но асинхронное поведение pure просто… лучше. Асинхронный git status можно реализовать и в собственном приглашении, но pure решает это достаточно аккуратно для моих задач.
Сам терминал
Скорость запуска оболочки — лишь половина истории: эмулятор терминала вносит собственную задержку ввода. Я использую Ghostty — он GPU-ускоренный и нативный, а вся моя конфигурация умещается в семь строк. В паре с алиасом tmux new -A -s main (сокращённым до t) новое окно терминала сразу возвращает меня в существующую сессию.
Как измерить производительность своей оболочки
Необязательно верить мне на слово — можно самостоятельно измерить, куда уходит время в вашем терминале. Следует искать три вида задержек: время запуска, задержку приглашения и задержку ввода.
Запустите несколько раз (первый запуск всегда медленнее из-за холодного кэша):
time zsh -i -c exit
Считаю, что всё до 100 мс — нормально, до 50 мс — отлично. Если вы видите 500 мс и больше, есть над чем поработать.
Для получения корректной статистики используйте hyperfine:
hyperfine --warmup 3 'zsh -i -c exit'
В Zsh встроен профилировщик. Добавьте в самое начало .zshrc:
zmodload zsh/zprof
а в самый конец:
zprof
Откройте новую оболочку — и получите отсортированную таблицу с точным указанием, куда ушло время. Первые строки обычно занимают compinit, подключение nvm.sh или какой-нибудь eval "$(…)".. Исправьте первое место, перезапустите, повторяйте. Когда закончите — удалите обе добавленные строки.
Если zprof недостаточно детален, можно трассировать весь запуск с временными метками:
zsh -ixc exit 2>&1 | ts -i '%.s' | sort -rn | head -20
Или задайте PS4='+%D{%s.%6.}: ' и запустите zsh -ixc exit 2> startup.log, затем ищите большие скачки между строками.
Запуск может быть быстрым, а каждая перерисовка приглашения — медленной. Перейдите (cd) в свой самый большой git-репозиторий и нажмите Enter: если перед появлением следующего приглашения есть задержка, значит приглашение выполняет синхронную работу, которая его тормозит. Можно либо перейти на асинхронное приглашение, либо отказаться от отображения информации о git.
Итого
Большинство этих оптимизаций — об исключении лишнего. Речь идёт об осознанном подходе: добавлять только то, чем будешь пользоваться. Каждая из десятков сессий, которые я открываю за день, запускается мгновенно, и терминал ощущается как продолжение мыслей, а не как приложение, которого приходится ждать. Для инструмента, с которым я провожу весь рабочий день, это не обсуждается.
Всё описанное выше живёт в моём репозитории dotfiles — берите, что нужно.