Making Zsh Load Instantly

5/9/23 All posts

I spend a lot of time in my shell, so any improvements I can make to it translate directly to productivity and improved QOL. There are a few objectives I always have: it needs to look nice, not sacrifice any functionality, and load quickly. The latter objective had gone neglected for a while, so while I was procrastinating studying for AP exams this week I decided to fix that.

Profiling

I used a couple of methods to find bottlenecks when figuring out what needed fixing. The first was zprof, which you can initialize by adding zmodload zsh/zprof to your zshrc. I also use a simple for loop to source my zshrc and print out the time: zload () { for i in $(seq 1 10); do /usr/bin/time $SHELL -i -c exit | grep "real"; done; }. The last is the timing + sorting method from the excellent article here.

Profiling findings

The biggest speed bumps were Python initialization (which conda adds to zshrc by default) and running Neofetch at terminal startup. Python was an easy fix - I moved the Python init code into a function that I called when first running a Python command in my shell:

if [ "$CONDA_INITIALIZED" = 0 ]
then
    # Conda initialization
    __conda_setup="$("$HOME/miniforge3/bin/conda" 'shell.zsh' 'hook' 2> /dev/null)"
    if [ $? -eq 0 ]; then
        eval "$__conda_setup"
    else
        if [ -f "$HOME/miniforge3/etc/profile.d/conda.sh" ]; then
            . "$HOME/miniforge3/etc/profile.d/conda.sh"
        else
            export PATH="$HOME/miniforge3/bin:$PATH"
        fi
    fi
unset __conda_setup
# Sets environment variable so that Conda setup is not run again
export CONDA_INITIALIZED=1

I then aliased python3 to py_init; python3 so that Python would only be initialized after I first called it. This got the speed down to under a second for real, and to ~1/4 of a second for user.

Results with neofetch and lazy py

Optimizing Neofetch

There wasn't a way to speed up Neofetch itself, so I decided to switch to fastfetch, a much faster alternative written in C. I also made a fork, found here, which customizes the flashfetch command that's configured at compile time.

Other changes

The final change I made was initializing as much as possible using zsh-defer. This is a plugin that allows you to defer commands until after the prompt is displayed, which makes the shell feel much faster. I used it to defer a number of commands, like sourcing zsh-syntax-highlighting and some config of Zsh's vim mode. Some of these deferred commands are technically unsafe, but they work in my current setup.

Results

After making these changes, my shell's user time is ~0.00 seconds, and the real time is ~0.05 seconds. Around 0.1 seconds is considered the threshold for what feels instantaneous, so I'm very happy with the results. I've included screenshots of the final load time and the load time of a blank zshrc below for reference, and my entire zsh configuration can be found on Github here.

Current shell load time

Results with a blank zshrc