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:

zmodload zsh/datetime

logfile=$(mktemp zsh_profile.XXXXXXXX)
echo "Logging to $logfile"
exec 3>&2 2>$logfile

setopt XTRACE

And add this code to the bottom:

unsetopt XTRACE
exec 2>&3 3>&-

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:

#!/usr/bin/env zsh

typeset -a lines
typeset -i prev_time=0
typeset prev_command

while read line; do
    if [[ $line =~ '^.*\+([0-9]{10})\.([0-9]{6})[0-9]* (.+)' ]]; then
        integer this_time=$match[1]$match[2]

        if [[ $prev_time -gt 0 ]]; then
            time_difference=$(( $this_time - $prev_time ))
            lines+="$time_difference $prev_command"


        local this_command=$match[3]
        if [[ ${#this_command} -le 80 ]]; then
done < ${1:-/dev/stdin}

print -l ${(@On)lines}

You can also just right-click and download this link. The script is hosted on Gist, where there is much more detail of exactly how the script works.1

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:


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.

  1. 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… ↩︎