Life is too short for a slow terminal

Life is too short for a slow terminal

生命苦短,拒绝迟钝的终端

June 6, 2026 2026年6月6日

Practically all of my work happens inside a terminal. Git, kubectl, tmux, ssh’ing into a server, open practically the entire day. Something I use that much has to be fast. Any lag in opening a new tab, typing a character or hitting tab for a completion is something I feel hundreds of times a day. It’s death by a thousand cuts. 我几乎所有的工作都在终端内完成。Git、kubectl、tmux、SSH 连接服务器,这些窗口几乎全天开启。既然使用频率如此之高,它就必须足够快。打开新标签页、输入字符或按下 Tab 键补全时的任何延迟,我每天都会感受到成百上千次。这简直是“千刀万剐”般的折磨。

My shell starts in about 30 milliseconds: 我的 Shell 启动时间大约在 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
...

That’s a fully loaded interactive shell with completions, syntax highlighting, autosuggestions, fzf and direnv, in less time than a single frame at 30fps. A new tab is instant. There was never some big optimization project behind this either; I’ve just always kept my shell minimal and fast and over the years that turned into a habit. Here’s how I go about it, and all of it can be found in my dotfiles. 这是一个功能完备的交互式 Shell,集成了补全、语法高亮、自动建议、fzf 和 direnv,其启动时间甚至不到 30fps 下单帧时长的一半。打开新标签页是瞬间完成的。这背后并没有什么宏大的优化工程;我只是始终保持 Shell 的简洁与高效,多年下来这已成为一种习惯。以下是我的做法,所有配置都可以在我的 dotfiles 中找到。

No framework

拒绝框架

The single biggest win is what’s not there: no oh-my-zsh, no prezto or plugin manager. I’ve honestly never understood the appeal of these frameworks. People install oh-my-zsh with its hundreds of plugins and themes, end up using maybe 5% of what it offers, and then pay (with their time and compute resources) for the other 95% every single time they open a shell. And plugin managers add their own overhead on top of that. 最大的收益在于“做减法”:没有 oh-my-zsh,没有 prezto,也没有插件管理器。老实说,我从未理解这些框架的吸引力所在。人们安装了带有数百个插件和主题的 oh-my-zsh,最终可能只用了其中 5% 的功能,却在每次打开 Shell 时都要为剩下的 95% 付出代价(消耗时间和计算资源)。而且,插件管理器本身还会带来额外的开销。

I use exactly three plugins, git-cloned once by my install script and sourced from .zshrc: 我只用了三个插件,由安装脚本一次性 git clone 下来,并在 .zshrc 中加载:

source ~/.zsh/fzf-tab/fzf-tab.plugin.zsh
source ~/.zsh/zsh-autosuggestions/zsh-autosuggestions.zsh
source ~/.zsh/zsh-syntax-highlighting/zsh-syntax-highlighting.zsh

There’s no plugin manager doing dependency resolution at startup, and a source of a file that’s already on disk is practically free. 启动时没有插件管理器进行依赖解析,而加载一个已经在磁盘上的文件几乎是零成本的。

Caching completions

缓存补全

compinit is one of the most expensive things in a typical .zshrc. By default it does a security audit of every completion file, every single time you open a shell. The fix is to only do the full run if the cache (.zcompdump) is older than 24 hours, and otherwise skip the check with -C: compinit 是典型的 .zshrc 中最耗时的操作之一。默认情况下,它会在每次打开 Shell 时对每个补全文件进行安全审计。解决方法是:仅当缓存(.zcompdump)超过 24 小时未更新时才执行完整运行,否则使用 -C 参数跳过检查:

autoload -Uz compinit
if [[ -n ~/.zcompdump(#qNmh-24) ]]; then
  compinit -C
else
  compinit
fi

That glob qualifier (#qNmh-24) reads as “exists and was modified within the last 24 hours”. So one full compinit per day, and cached reads the rest of the time. 那个 glob 限定符 (#qNmh-24) 的意思是“文件存在且在过去 24 小时内被修改过”。这样每天只需进行一次完整的 compinit,其余时间都读取缓存。

Lazy-loading

延迟加载

nvm is probably the most notorious shell startup killer out there; sourcing it eagerly can easily add half a second. But I don’t need nvm in every shell, I need it when I type nvm. So I wrap it in a function that replaces itself on first use: nvm 可能是最臭名昭著的 Shell 启动杀手;急切地加载它很容易增加半秒的启动时间。但我并不需要在每个 Shell 中都用到 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 "$@"
}

The first nvm call deletes the stub, sources the real thing (with —no-use so it doesn’t resolve a node version either), and forwards the arguments. Same idea for kubectl completions, which shell out to the kubectl binary to generate the completion script. I only load them after the first time I actually run kubectl: 第一次调用 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
}

This pattern works for a lot of things: anything that tells you to put eval ”$(tool init zsh)” in your .zshrc is a candidate for lazy loading, because each of those forks a process and evaluates its output at startup. I keep direnv and fzf eager because they’re fast and I use them constantly. Be strict about what you actually use a lot. 这种模式适用于很多场景:任何要求你在 .zshrc 中添加 eval ”$(tool init zsh)” 的工具都是延迟加载的候选对象,因为它们中的每一个都会在启动时 fork 一个进程并评估其输出。我保留了 direnv 和 fzf 的急切加载,因为它们很快且我经常使用。对于你真正高频使用的工具,要严格把控。

A non-blocking prompt

非阻塞提示符

A prompt that runs git status synchronously will lag in any decently sized repo. That’s the kind of lag you feel on every single Enter press, which is arguably worse than a slow startup. I use pure, which renders the prompt immediately and fills in the git info asynchronously when it’s ready. I briefly tried replacing it with zsh’s built-in vcs_info, but pure’s async behavior is just… better. You can do async git status in your own prompt as well, but pure wraps it rather nicely for my use-case. 如果提示符同步运行 git status,在任何稍大一点的仓库中都会产生延迟。这种延迟在你每次按下回车键时都能感觉到,这比启动缓慢更糟糕。我使用 pure,它会立即渲染提示符,并在 git 信息准备好后异步填充。我曾短暂尝试用 zsh 内置的 vcs_info 替换它,但 pure 的异步行为显然更好。你也可以在自己的提示符中实现异步 git status,但 pure 对我的使用场景封装得非常出色。

The terminal itself

终端本身

Shell startup is only half the story, because the emulator adds its own input latency. I use Ghostty, which is GPU-accelerated and native, and my config is just seven lines long. Combined with a tmux new -A -s main alias (t), a fresh terminal window drops me right back into my existing session. Shell 启动只是故事的一半,因为终端模拟器本身也会增加输入延迟。我使用 Ghostty,它是 GPU 加速的原生应用,我的配置只有七行。配合 tmux new -A -s main 的别名 (t),打开一个新的终端窗口就能让我直接回到现有的会话中。

Measuring your own shell performance

测量你自己的 Shell 性能

You don’t have to take my word for any of this, you can measure where the time goes in your own terminal. There are three kinds of lag to look for: startup time, prompt lag, and input latency. Run this a few times (the first run is always slower because of cold caches): 你不必盲目相信我的话,你可以亲自测量终端的耗时情况。需要关注三种延迟:启动时间、提示符延迟和输入延迟。运行以下命令几次(由于缓存未命中,第一次运行总是较慢):

time zsh -i -c exit

I think anything under 100ms is fine, and under 50ms is great. If you’re seeing 500ms or more you have some work to do. For proper statistics, use hyperfine: 我认为 100ms 以下是可以接受的,50ms 以下则是极好的。如果你看到 500ms 或更多,那你得好好优化一下了。若要进行精确统计,请使用 hyperfine:

hyperfine --warmup 3 'zsh -i -c exit'

Zsh also ships with a profiler. Put this at the very top of your .zshrc: Zsh 还自带了一个性能分析器。在 .zshrc 的最顶部添加:

zmodload zsh/zprof

and this at the very bottom: 并在最底部添加:

zprof

Open a new shell and you get a sorted table of exactly where the time went. The top entries are usually compinit, an nvm.sh source, or some eval ”$(…)”. Fix the top one, re-run, repeat. Remove both lines when you’re done. 打开一个新的 Shell,你就会得到一张按耗时排序的表格。排在前面的通常是 compinit、nvm.sh 的加载,或者某些 eval ”$(…)”。修复最耗时的那个,重新运行,重复此过程。完成后删除这两行代码即可。

If zprof isn’t granular enough, you can trace the whole startup with timestamps: 如果 zprof 的粒度不够细,你可以用时间戳追踪整个启动过程:

zsh -ixc exit 2>&1 | ts -i '%.s' | sort -rn | head -20

Or set PS4=’+%D{%s.%6.}: ’ and run zsh -ixc exit 2> startup.log, then look for the big jumps between lines. Startup can be fast while every prompt redraw is slow. cd into your biggest git repo and press Enter; if there’s a delay before the next prompt appears, your prompt is doing synchronous work slowing it down. You can either switch to using an async prompt, or opt to strip out the Git functionality. 或者设置 PS4=’+%D{%s.%6.}: ’ 并运行 zsh -ixc exit 2> startup.log,然后查看行与行之间的时间跳跃。启动可能很快,但每次重绘提示符可能很慢。进入你最大的 git 仓库并按下回车;如果下一个提示符出现前有延迟,说明你的提示符正在进行同步操作。你可以切换到异步提示符,或者选择剔除 Git 功能。

Wrapping up

总结

Most of these optimizations are about leaving stuff out. It’s about… 这些优化大部分都是关于“做减法”。这关乎于……