Recently, I’ve been trying to make my coding environment snappier, and one thing I was never happy with was how slow my .zshrc is.
Now, don’t get me wrong, I’m not one of those people using oh-my-zsh with a ton of plugins and seeing 15-second waits for my shell to start… but I do want a new tab to be ready in a second or less.
So, I slapped zmodload zsh/zprof onto the top of my .zshrc, opened a new tab, and ran zprof | less …and 50% of the wait was in sourcing virtualenvwrapper, which I don’t feel like reinventing.
Time to take a lesson from the improvements I’ve been making to my .vimrc. Specifically, the { 'on': ['CommandA', 'CommandB'] } option hanging off the end of various lines for my plugin loader.
A little experimentation later and I came up with this construct:
function init_virtualenvwrapper {
# Don't do anything if it's already loaded
type virtualenvwrapper_workon_help &>/dev/null && return
# ------------------------------------------------
# normal stuff to load virtualenvwrapper goes here
# ------------------------------------------------
}
for cmd in workon mkproject mkvirtualenv; do
function $cmd {
unset -f "$0"
init_virtualenvwrapper
"$0" "$@"
}
done
For those not familiar with shell scripting, I’ll clarify.
For each shell function or command that I want to trigger deferred loading, I create a function with the same name that does the following:
- “Delete” itself so it won’t interfere with what virtualenvwrapper is going to set up. (You want to do this first to avoid removing what virtualenvwrapper just created)
- Call the virtualenvwrapper setup code to load the real command.
init_virtualenvwrapperstarts by checking for some side-effect of having been run before and exits early if that’s the case. (This keepsmkprojectfrom re-doing whatworkonalready did, or vice-versa.)- Call the actual command and pass through any arguments.
Doing this means that:
- Your
.zshrcor.bashrcstartup time only pays the price for declaring a few shell functions. (And, if that gets too heavy for some reason, you could moveinit_virtualenvwrapperinto another file andsourceit on demand.) - Your first call to a wrapped command like
workonwill take longer. (eg. if it was adding two seconds to your shell start time, then your first call to it will take two seconds longer.) - Subsequent calls to that or any other command sharing the same
init_virtualenvwrapperwill be as quick as usual.
Unfortunately, this design is actually Zsh-specific, which sucks for me because this is a file I share between .zshrc and .bashrc:
- Bash doesn’t support using a variable for a function name, so you can’t use a
forloop. You’ll just get`$cmd': not a valid identifier. - In my testing, functions didn’t set
$0in bash, so this will actually executebash "$@", bringing you back to where you started, while zsh doesn’t set theFUNCNAMEarray variable that bash uses.
So, if you want to support both, here’s the most concise form I was able to put together:
function init_virtualenvwrapper {
local _cmdname="$1"
shift
unset -f "$_cmdname"
# Don't do anything if it's already loaded
if ! type virtualenvwrapper_workon_help &>/dev/null; then
# ----------------------------------------
# normal stuff to load virtualenvwrapper
# ----------------------------------------
fi
"$_cmdname" "$@"
}
# }}}
function workon {
init_virtualenvwrapper "${FUNCNAME[0]:-$0}" "$@"
}
function mkproject {
init_virtualenvwrapper "${FUNCNAME[0]:-$0}" "$@"
}
function mkvirtualenv {
init_virtualenvwrapper "${FUNCNAME[0]:-$0}" "$@"
}
Anyway, I hope this helps to inspire anyone else who’s suffering from slow shell startup times.
UPDATE: And now, shortly after writing that, I discover that someone else went to the trouble of using eval to provide a nice API on top of this trick and put it up on GitHub as sandboxd. From that name, I can see why i didn’t find it before.
On-Demand Loading for your .zshrc or .bashrc by Stephan Sokolow is licensed under a Creative Commons Attribution-ShareAlike 4.0 International License.

You could also check out the `zinit` plugin manager, which provides a way to postpone the sourcing of your plugins and doing so in the background (i.e. it provides similar features to your vim plugin manager’s lazy loading).
It does have some cognitive overhead to get it working just so, but maybe it will be of use to you (or someone else reading this post).
Under normal circumstances, that’d be true, but my zshrc is completely hand-written and aggressively optimized. Just adding
zinitwould be a step backward before I’ve even added anything for it to load.I’ve got it to the point where, with a warm cache,
zprofreportscompinit -Ctaking between 77% and 96.6% of.zshrc‘s runtime… and-Ctellscompinitto just load the cached completion dumpfile without checking if it’s stale to minimize disk I/O.For cold-cache starts, I’m using
"${(%):-"%D{%s}"}"to query the wall time without invoking a subprocess and skipping my usualfortunecommand if it takes 2 seconds or more to get from the top of my .zshrc to the bottom. (I couldn’t find a way to get sub-second resolution without invoking a subprocess.)Still, I’m sure it’ll be useful to someone else, so thanks for mentioning it.