Faster Bash Startup

My Bash start-up time is around 1.7 seconds. This doesn’t sound like it’s too bad, but it feels like hours when you want to open a new window to only run one command.

The first step is to run a profile of your Bash start-up, to try and find out what exactly is taking so long.

How to profile a bash shell script slow startup?

This should give you a timestamp against each Bash command that gets run, which you can use to work out which ones are taking the most time.

For me, there are a few things that were highlighted.

Note: Everything here will be specific to my setup (MacOS, Alacitty, Bash, Tmux) and so may and probably will differ.

I tested the difference between each change by using hyperfine.

hyperfine 'bash --login'

Dynamic paths

One of the big culprits for slowness is trying to be too clever. For example, Homebrew has a prefix directory where it installs everything, and environment variables that are set are set relative to this directory.

For example, you might have some code that looks like this:

if [ -f $(brew --prefix)/etc/bash_completion ]; then
    . $(brew --prefix)/etc/bash_completion
fi

To allow for changing the default prefix directory, there is a command brew --prefix that will resolve to where the prefix directory is. However, this command can be quite slow and just replacing it with a hardcoded path can give you a decent speed up:

Benchmark #1: bash prefix.sh
  Time (mean ± σ):     289.9 ms ±   4.5 ms    [User: 178.9 ms, System: 103.6 ms]
  Range (min … max):   285.4 ms … 300.7 ms    10 runs

Benchmark #2: bash no_prefix.sh
  Time (mean ± σ):     236.1 ms ±   2.4 ms    [User: 157.7 ms, System: 75.9 ms]
  Range (min … max):   231.6 ms … 241.8 ms    12 runs

Summary
  'bash no_prefix.sh' ran
    1.23 ± 0.02 times faster than 'bash prefix.sh'

This is even more significant if you have a prefix command that specifies a particular library. E.g:

if [ -f $(brew --prefix openssl) ]; then
    OPENSSL_PREFIX="$(brew --prefix openssl)"
    export OPENSSL_INCLUDE_DIR="${OPENSSL_PREFIX}/include"
    export OPENSSL_LIB_DIR="${OPENSSL_PREFIX}/lib"
fi

In this case, using a hard coded path has a significant speed up.

Benchmark #1: bash prefix.sh
  Time (mean ± σ):      1.095 s ±  0.015 s    [User: 627.0 ms, System: 444.6 ms]
  Range (min … max):    1.079 s …  1.131 s    10 runs

Benchmark #2: bash no_prefix.sh
  Time (mean ± σ):       3.1 ms ±   0.4 ms    [User: 1.2 ms, System: 1.0 ms]
  Range (min … max):     2.5 ms …   5.4 ms    389 runs

  Warning: Command took less than 5 ms to complete. Results might be inaccurate.

Summary
  'bash no_prefix.sh' ran
  358.57 ± 47.07 times faster than 'bash prefix.sh'

Lesson: Avoid $(give_me_a_path) where possible by using a hard coded path. Especially avoid brew --prefix <formula>.

Bash completions

The other major slow down in my Bash start-up was Bash completions.

Benchmark #1: source .profile
  Time (mean ± σ):     208.6 ms ±   5.7 ms    [User: 90.0 ms, System: 115.6 ms]
  Range (min … max):   203.7 ms … 223.7 ms    13 runs

Benchmark #2: source .profile && source /usr/local/etc/bash_completion
  Time (mean ± σ):     442.3 ms ±   3.3 ms    [User: 240.2 ms, System: 198.4 ms]
  Range (min … max):   438.1 ms … 449.2 ms    10 runs

Summary
  'source .profile' ran
    2.12 ± 0.06 times faster than 'source .profile && source /usr/local/etc/bash_completion'

As you can see above, adding completions doubles the start-up time of my Bash session. I honestly don’t know if I have ever even used them, so it was an easy trade-off for me to make by removing them for a bit more speed. If I had used them I would have thought more carefully about it.

Conclusion

Using variations on the above I’ve managed to shave quite a bit of time from my Bash startup (ignore the name change):

Benchmark #1: source ~/.bashrc
  Time (mean ± σ):      1.669 s ±  0.025 s    [User: 924.4 ms, System: 706.6 ms]
  Range (min … max):    1.644 s …  1.728 s    10 runs
Benchmark #1: source ~/.profile
  Time (mean ± σ):     216.2 ms ±   5.9 ms    [User: 94.2 ms, System: 119.5 ms]
  Range (min … max):   210.2 ms … 234.0 ms    13 runs

I could keep tweaking in an attempt to make it even faster, but anything else at this point is likely to suffer from diminishing returns. I mean, it has to take some time.