Life is too short for a slow terminal

76 points by npiazza a day ago on lobsters | 56 comments

icefox | a day ago

Pedantry time: you mostly mean shell, not terminal.

ryan-duve | a day ago

I used to use the words "shell" and "terminal" interchangeably. I still do, but I used to, too.

offby1 | a day ago

Upvoted for accuracy and Mitch-ness.

subnut | a day ago

Who's Mitch?

Halkcyon | 8 hours ago

invlpg | 4 hours ago

I didn't even know he was sick.

subnut | 8 hours ago

Ah, I remember watching this skit ages ago! Forgot the name... thanks for the link.

thomas536 | a day ago

mitch hedberg

classichasclass | 23 hours ago

sighs in front of 9600bps serial terminal

boramalper | a day ago

Life is too short to use tools without sensible defaults, use fish!

reissbaker | a day ago

I wish someone would make a bash-compatible fish, i.e., a shell with reasonable defaults, with fast completions built-in, that didn't require throwing away or rewriting bash-based tooling.

weaksauce | a day ago

what bash tooling are you losing by using fish? i haven't had any issues at all with anything.

Halkcyon | 8 hours ago

IME, source file.sh or being able to copy/paste scripts to the interactive shell. Many tools have --fish options now, but it's not complete, so I still find myself needing to parse bash scripts to convert them to fish scripts dynamically (this usually involves executing the bash if it's dynamic, exporting its environment, re-importing to fish).

weaksauce | 2 hours ago

I guess i either run the bash script itself with a shebang or just haven't seen anything that i needed from bash world. ootb fish is quite useable and doesn't need all the extra faffing about with configurations.

quasi_qua_quasi | 16 minutes ago

zimpenfish | a day ago

Isn't that mostly what osh does?

LolPython | a day ago

Per your link, I thought that was focused on scripting?

dpercy | a day ago

If you mean tools like nvm, which require sourcing into your shell, I’ve found bass to be a really helpful adapter: https://github.com/edc/bass

kablamooo | 19 hours ago

I mean, you could use fish and then use bass to add bash compatibility.

kablamooo | 19 hours ago

This was a huge upgrade for me over bash and zsh even with tons of tweaking. Its packages so much that should just be there and the plugin ecosystem is good for the tiny number of things I might want.

LAC-Tech | 19 hours ago

I switched to fish a couple of years back. It's my default shell now.

Thrilled to be living in the 90s.

[OP] npiazza | a day ago

My ZSH at work got ridiculously slow about a year ago, which prompted me to try fish! Really loving its quality of life features and out of the box support for stuff like modern tab completions where you can use the arrow keys.

I still run ZSH on my personal boxes, but that’s just out of laziness lack of time to mess with my Nix config and home manager.

fedemp | a day ago

Life is too short to install new tools. Just give me sensible defaults. :)

invlpg | 4 hours ago

I used fish for years until I moved to jobs that require me to regularly ssh into remote systems. Since then I've stuck with either bash or OpenBSD ksh.

Life is too short to constantly switch between them. And I say that as someone who has to regularly switch between multiple keyboard layouts, languages (programming and natural), and operating systems.

Having a mostly-consistent, albeit shit, primary interface to the systems you work on cannot be underestimated.

WilhelmVonWeiner | 3 hours ago

I just install fish on remote systems, honestly.

invlpg | 3 hours ago

And that's not an option for many of us for a multitude of reasons.

vimpostor | a day ago

time zsh -i -c exit

I hate how this broken "benchmark" is still so commonly used. It measures the completely wrong thing, and some zsh plugin managers have even optimized for this useless metric, often at the cost of actual shell startup latency.

The zsh-bench utility has a whole section on why this benchmark is useless: https://github.com/romkatv/zsh-bench#how-not-to-benchmark

The metrics that zsh-bench measures are far more useful, such as first prompt lag and input latency.

mhd | a day ago

I sometimes wonder whether all this work ("non-blocking prompts" and OpenGL-based terminals especially) are worth it compared to just doing xterm & PS1="\W: "

dsr | a day ago

I hadn't used xterm on purpose for years, but then I reviewed a whole bunch of terminal emulators and was quite surprised to discover that xterm fully supports:

  • opentype fonts
  • UTF-8
  • most stupid emoji icons
  • 24 bit color
  • low memory usage compared to almost everything else

and has the dual advantages of being very fast and "the standard", so any bugs remaining are likely inconsequential or considered normal behavior by all programs running in it.

So now I use xterm again.

sjamaan | a day ago

Only thing about xterm is it doesn’t reflow text on resize. When using a tiled WM that can get very annoying quickly.

dsr | a day ago

What is it about using a tiled WM that makes you change terminal sizes often?

(I don't use a tiled WM, I use a conventional WM with a large number of workspaces, each dedicated to a particular task, and with keyboard shortcuts for each.)

sjamaan | a day ago

What the others said doesn’t really apply to my workflow. For me I like to toggle between fullscreen and half width a lot (using i3)

iamnearlythere | a day ago

Depends on the setup, but I usually go with the default layout in i3, which makes all windows share the same space (i.e. existing windows shrink then a new window is opened, 100% horizontal w for 1 window, 50 for two, etc.), until you start splitting or changing layout to stacked (maybe i3 terminology, unsure).

patryk | a day ago

every time you spawn new window - most of other windows are resized to fit your display. Lack of reflow is especially painful when you don't use any terminal multiplexer (e.g. because it's just another window manager and you don't want to end up with 3 layers of them, counting (neo)vim/emacs)

quasi_qua_quasi | 23 hours ago

Honestly IMO "stacked/tabbed/etc" is the default worked way better for me than splits as the default.

donio | a day ago

It's not. zsh startup is very fast, it only becomes slow if you make it slow. There is nothing magical about it. Just don't add a bunch of stuff that you don't understand. That includes libraries that call themselves minimal and then run hundreds of commands to build each prompt.

My zsh config has been very slowly evolving since the 90s. It's a couple hundred lines and if I look through it I understand every line and I know why it's in there. I've never done anything specifically to make it faster and it still starts up in 20ms. I know how quickly it's supposed to pop up and if I ever do something dumb to slow it down I'd notice and fix it immediately.

quasi_qua_quasi | 23 hours ago

I wrote my entire fish prompt by myself and I made it an explicit goal to keep the prompt time under 5ms (120 Hz framerate plus a little allowance for overhead). The only actually fancy stuff is VCS status, where I use a subprocess + fish's built-in support for "set a variable shared by other fish instances" + "run a command when a variable changes" + "repaint prompt" as the IPC mechanism. But everything other than that is just simple echo calls and variable reads so I've never even gone above 1ms.

offby1 | 6 hours ago

I've never quite been able to wrap my head around that variable setting; would you be willing to share what you have?

quasi_qua_quasi | 5 hours ago

Ah, I forgot, I actually just used signals. https://codeberg.org/ext0l/mizu is what I have; I've been using it for a couple months now and it works fine.

I remember I initially thought about using the "variables as IPC" thing, but I decided against it. I don't remember why.

fedemp | a day ago

I though this would be about GPU accelerated terminal bug happy to see it's not.

Caching completions

Great tip. I only use zsh in my work machine which happens to be a Mac. For reasons unknown, this machine will show the beach ball even when thinking about opening a new tab, so hopefully this will make a difference.

Lazy-loading Same idea for kubectl completions, which shell out to the kubectl binary to generate the completion script.

What is the slow thing here? Generating the completitions, or sourcing them? Cause if it's the former, would saving them to a file and then sourcing it help with start up time? Cause hat's what I do with jj for example.

A prompt that runs git status

I ditched it when moved to jj. :D

Measuring your own shell performance

I wish author had shared their times; maybe I'm on the average or maybe I'm on the slow.

real	0m0.287s
user	0m0.136s
sys	0m0.195s

Edit: I tried to cut some time and these were results:

A mostly empty .bashrc only setting some options and the prompt.

real	0m0.007s
user	0m0.003s
sys	0m0.004s

After enabling skim keybindings

real	0m0.043s
user	0m0.019s
sys	0m0.049s

After enabling mise

real	0m0.115s
user	0m0.054s
sys	0m0.097s

After enabling jj completitions (even for bookmarks)

real	0m0.186s
user	0m0.086s
sys	0m0.138s

After adding source /etc/bashrc to my bashrc

real	0m0.294s
user	0m0.146s
sys	0m0.192s

So there seems to be room for improvement. I highly doubt I need the global /etc/bashrc. Maybe I can manually add mise executables to my $PATH and call mise activate when I'm in a project folder. And author also mentions defering completitions until the first time a command is called; I should do that for jj.

adam_d_ruppe | a day ago

Author said 30 ms for the shell itself up front. Mine comes in at about 50ms doing the same time shell -c exit test.

When I have to use other people's linux setups, the thing that drives me the most nuts are the stupid pointless animations all over the place. On my computer, I hit the hotkey, the terminal window is open almost instantly. Sometimes I can see a brief blink between the window and the prompt but it even that not always.

So doing a full end-to-end test, opening new window with shell, doing something in it, and closing it is what I care about. So i did time myterm and hit ctrl+d in the window to make it disappear all consistently in under 0.120s

Amazing how many things just not having pointless animations and compositions open up to me. I had to look for difference between two spreadsheets a few months ago... I just maximized both in separate windows and hit the window shade hotkey a couple times to flip-o-rama them. The difference was instantly visible. Trying to do the same thing on Windows with Excel doing the weird animations were just too distracting.

patryk | a day ago

I wish author had shared their times; maybe I'm on the average or maybe I'm on the slow.

yeah, sub-100ms seems like a no-go for me :D

$ ls -lsha; ZDOTDIR=$(pwd) hyperfine -i --warmup 3 'zsh -i -c exit'
total 0
0 drwx------.  2 pg   pg    40 Jun  6 17:20 .
0 drwxrwxrwt. 35 root root 820 Jun  6 17:20 ..
Benchmark 1: zsh -i -c exit
  Time (mean ± σ):     129.8 ms ±   3.5 ms    [User: 51.2 ms, System: 83.3 ms]
  Range (min … max):   123.6 ms … 135.9 ms    23 runs

and my "full" config takes about the same as yours to load - about 250ms (I've managed to shave off like ~5ms on average, mostly with compinit caching, but it's not worth the effort in my opinion, as it may cause missing completions)

ndegruchy | a day ago

I went through some of this and found out that my zsh-abbr is eating ~100ms of startup time, but I'm perfectly okay with that. I can shave off ~10ms here and there, but none of it seems like it's worth it for the functionality I lose.

I'll live with the ~300ms startup time, as it's fast enough and I don't tend to need to throw open terminals in rapid succession, or need to type immediately.

Great post, though. Taught me about hyperfine and I dug into some of the startup files for zsh.

oftenwrong | a day ago

I recently had my zsh startup slow to a crawl. I didn't pinpoint the root cause, but I did discover that compinit was dominating the critical path, and I implemented caching in approximately the same way as suggested in the post in order to eliminate the slowness.

I am going to have to improve mine with that amazing glob qualifier. I did not even know that was possible. It honestly seems like a questionable feature, but I'll still use it. I was using the comparatively unrefined date -Id approach for constructing the target path.

I love tools like zsh that are configured with a full power programming language. There is no need for the author to add caching features because we can implement it ourselves.

In my nearly 20 years of using zsh, I have never used a framework or plugin manager. It seems like one of the main reasons these are used is for styling. I guess am lucky that I do not care about aesthetics in my computing environment. I have a hand-rolled custom prompt that is pretty basic, compact, informative, and not at all sexy. I use the default terminal theme with a black background.

vbernat | a day ago

Caching compinit is one of the frustrating thing because the cache can be outdated. Also, you may have several instances of the shell doing the same work in parallel (it happened to me a lot when spawning parallel instances in tmux for a lab). And you can share your home between several hosts (notably containers). I end up with this.

() {
    emulate -L zsh
    [[ -o interactive ]] || return
    setopt extendedglob
    autoload -Uz compinit complist
    local -a fpp
    fpp=($^fpath/**/*(N.))
    local zcd=$ZSHRUN/zcompdump-${ZSH_VERSION}-${#fpp}
    local zcdc=$zcd.zwc           # compiled compdump
    local zcda=$zcd.last          # last compilation
    local zcdl=$zcd.lock          # lock file
    local attempts=30
    : >> $zcd
    while (( attempts-- > 0 )) && ! ln -s $zcd $zcdl 2> /dev/null; do sleep 0.1; done
    {
        if [[ ! -e $zcda || -n $zcda(#qN.mh+24) ]]; then
            print -nu2 "Building completion cache..."
            # No compdump or too old
            \rm -f $ZSHRUN/zcompdump*(N.mM+6)
            compinit -u -d $zcd
            : > $zcda
            print -nu2 '\r'
        else
            # Reuse existing one
            compinit -C -d $zcd
        fi
        [[ ! -f $zcdc || $zcd -nt $zcdc ]] && rm -f $zcdc && {
                # On 9p, this fails because O_CREAT|O_WRONLY, 0444 fails
                if [[ $(findmnt -no FSTYPE $(stat -c %m $zcd 2> /dev/null) 2> /dev/null) != "9p" ]]; then
                    zcompile $zcd &!
                fi
            }
    } always {
        \rm -f $zcdl
    }
}

[OP] npiazza | a day ago

My ZSH load times got bad enough where I just decided to try fish. I think I’ve slowly moved my fish config in the same direction, unfortunately. On a break on Monday I’m gonna dig into profiling and see if that lazy loading trick is actually useful in my case.

I suspect most of my slowdown time is the git modules in Starship, but I do have a fair amount of aliases and helper functions that could be lazy loaded instead.

neeasade | a day ago

for a long time what I've done in emacs is initialize a background "staging shell" - then opening a terminal == open a new window with that buffer and rename it - then fork off a thread for re-staging a shell for next time. no startup delay yipeeeeee

edit: I recall trying to finagle a non-emacs solution with reptyr[1] at some point, but ended up not committing to that route, I don't remember why.

[1] https://github.com/nelhage/reptyr

0x2ba22e11 | a day ago

Ooh like the zygote process in androidz nice

Awesome! Made some long-delayed changes to my zshrc. Have it at 80ms now and it's great!

bazzargh | 22 hours ago

My zsh doesn't use oh-my-zsh either, I use a function lifted from https://github.com/mattmc3/zsh_unplugged

But the biggest thing that made my shell slow was the git ps1 when navigating large repos; it does a lot of work to colour in the prompt (pretty much the only colour I use in the shell is paren matching). These days I run with:

unset GIT_PS1_SHOWDIRTYSTATE
unset GIT_PS1_SHOWSTASHSTATE
unset GIT_PS1_SHOWUNTRACKEDFILES
unset GIT_PS1_SHOWUPSTREAM

I'm not sure if that's all still needed, but at the time I timed all the options hitting our multi-gigabyte repos and the defaults for these options all slowed down the prompt.

edwintorok | 11 hours ago

As written the compinit optimization doesn't work if your zcompdump is already older than 24h (in my case zcompdump was from May 30). Running compinit (without -C) doesn't appear to change the timestamp of the file when nothing changed (no new completion files or functions installed). It should either touch ~/.zcompdump after running compinit, or it should first delete the file before running compinit to ensure that a new timestamp is created.

cameron | 10 hours ago

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 should have done something like this years ago. it adds like 300ms

seachub | 4 hours ago

been using magit for like 15 years, and for the first time the $work repo is gigantic and "magit-status", which I run as a tick, takes like 1.5 seconds. I tried for a bit to fix with it with a LLM but couldn't figure it out.

cole-k | 3 hours ago

I wonder if because it's a part of emacs, you could track which files you've touched (in emacs) since the last status and (assuming the branch is unchanged) just re-diff only those that you've edited. But I don't know the intricacies of what other things happen when git status is run.

(and ofc you'd want this to be an opt-in feature since changes made outside of emacs won't get picked up, etc. etc.)

vlnn | a day ago

My life is long enough for a slow terminal, sometimes I'd prefer terminals being even slower (e.g. imagine making 5 sec delay default for execution in root console before real execution, just to have ctrl-c buffer for mistypes — it would perhaps save me days in my rebel youth).

metasim | 21 minutes ago

I’ve been coding for almost 40 years. Terminal speed has never been a problem for me since modems went away. What am I missing here?