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_virtualenvwrapper
starts by checking for some side-effect of having been run before and exits early if that’s the case. (This keepsmkproject
from re-doing whatworkon
already did, or vice-versa.)- Call the actual command and pass through any arguments.
Doing this means that:
- Your
.zshrc
or.bashrc
startup time only pays the price for declaring a few shell functions. (And, if that gets too heavy for some reason, you could moveinit_virtualenvwrapper
into another file andsource
it on demand.) - Your first call to a wrapped command like
workon
will 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_virtualenvwrapper
will 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
for
loop. You’ll just get`$cmd': not a valid identifier
. - In my testing, functions didn’t set
$0
in bash, so this will actually executebash "$@"
, bringing you back to where you started, while zsh doesn’t set theFUNCNAME
array 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
zinit
would 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,
zprof
reportscompinit -C
taking between 77% and 96.6% of.zshrc
‘s runtime… and-C
tellscompinit
to 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 usualfortune
command 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.