I start new zsh instances all the time so long startup times become distracting quickly. Here’s how you can figure out which parts of your zshrc or zshenv are taking the most time to run.
Instrumenting your zshrc
Add this code to the top of your zshrc:
And add this code to the bottom:
Each time you start zsh, this code will create a new log file in the current directory (typically your home directory) and display the name of this log file. Each command will be written to the log file along with its start time in UNIX epoch format. This logging is turned off when your zshrc is done running. (I took this approach from a Stack Overflow answer by Dennis Williamson.)
You can profile your zshenv instead of your zshrc by adding the same commands to that file.
Now if you create a new zsh instance you’ll have a file called something like
zsh_profile.abcd1234 in your home directory. Copy the following script into a file called
sort_timings.zsh and make it executable:
Run your log file through this script with
./sort_timings.zsh zsh_profile.abcd1234 | head
Interpreting the result
This will list all of the commands that were executed in the course of running your zshrc, longest-running first. For example, here’s what the beginning of the output looked like for me:
465844 /Users/bdesham/.zshrc:148> brew --prefix nvm 438098 /Users/bdesham/.zshrc:146> [[ -e /usr/local/opt/nvm ]] 391026 nvm_die_on_prefix:50> NVM_NPM_PREFIX=/Users/bdesham/.nvm/versions/node/v6.9.1 221299 nvm_auto:12> VERSION=v6.9.1 100760 nvm:585> VERSION=v6.9.1 95807 nvm_ensure_version_installed:12> LOCAL_VERSION=v6.9.1 87509 nvm_version:21> VERSION=v6.9.1 86813 nvm_version:21> VERSION=v6.9.1 25094 nvm_resolve_local_alias:7> VERSION='' 24649 nvm_resolve_local_alias:7> VERSION='' 22581 /Users/bdesham/.zshrc:148> source /usr/local/opt/nvm/nvm.sh 20155 compaudit:60> _i_files=( /usr/local/share/zsh-completions/_ack /usr/local/sha... 19850 /Users/bdesham/.zshrc:61> stack --bash-completion-script /usr/local/bin/stack 18718 /Users/bdesham/.zshrc:59> stack --version 15506 nvm_die_on_prefix:51> nvm_tree_contains_path /Users/bdesham/.nvm /Users/bdesh... 13686 nvm_ls:29> PATTERN=v6.9.1 13366 nvm_ls:29> PATTERN=v6.9.1 12902 nvm:626> NVM_VERSION_DIR=/Users/bdesham/.nvm/versions/node/v6.9.1 12834 nvm_is_version_installed:1> nvm_version_path v6.9.1 12547 nvm_resolve_alias:15> ALIAS_TEMP=''
The first column shows the approximate amount of time, in microseconds, taken by each command. The next column shows the filename and line number of the zsh script that was being run. The rest of the line shows the command that was run.
Here are two things I learned from profiling my zshrc and zshenv:
Loading NVM, the Node Version Manager, added nearly two seconds to my shell startup time. This is a great tool if you’re working in Node all day, but I’m not anymore, so I moved the NVM loading code out of the main part of my zshrc into a function that I can call when needed.
The “right way” to get the path to a Homebrew-installed package is
brew --prefix. For example, if you’ve installed the GNU Coreutils with Homebrew and you want to make them available on your
PATH, you could do
path+=$(brew --prefix coreutils)/libexec/gnubin
However, running this
brewcommand takes nearly a second! It’s a much better idea just to hardcode the path. If Homebrew’s directory structure changes you can update the path in your zshenv and you’re probably still saving time overall.
This profiling code actually slows down zsh as it starts up, so once you’ve gotten the timing information and made some changes you’ll probably want to take it out again. (See the next section for a less detailed but easier way to measure the total zsh startup time.)
This method of profiling only measures individual commands; it doesn’t make a fancy tree that shows you, for example, the total time spent sourcing some file. This means that if you load a file that contains a million commands that each take ten microseconds, those commands are all going to show up at the bottom of this list even though in aggregate they’re taking a long time to run.
A faster way to measure the total zsh startup time
By the way, if you just want a measurement of the total time it takes to launch zsh, run
time zsh -i -c echo
The time labeled with “total” is the total shell startup time in seconds. The
-i flag forces zsh to load all of the files, like your zshrc, that it would usually load for an interactive session. The
-c echo tells zsh to run a dummy command—print a blank line—and immediately exit. This is handy if you want to make a change to your zshrc or zshenv and you just want to know the overall effect on the startup time.
I would usually write this kind of script in Awk, but writing it in portable Awk would have been clumsy, and hey, everyone in the target audience has zsh installed… ↩︎