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.