Iʼm using various auto-completions for tools like Docker, Kubernetes, the AWS CLI and Node.js. Over time, together with other initialization code snippets, starting a new shell got slower and slower. But then I found an easy way to get back to speed.

I stumbled upon a blog post about zsh lazy-loading and really liked the idea. The gist: You create a shell function with the same name as the binary you want to load. It has preference over executable files. The function takes care of additional setup and sourcing auto-completion, then executes the binary.

So far, so good, but in the example, the function was called each time instead of the binary, doing all the setup steps over and over again. Instead, you can unset the function after its first invocation and all subsequent calls will execute the binary directly. This example shows how it works for the kubectl command:

#!/usr/bin/env zsh

# Check if 'kubectl' is a command in $PATH
if [ $commands[kubectl] ]; then

# Placeholder 'kubectl' shell function:
# Will only be executed on the first call to 'kubectl'
kubectl() {

# Remove this function, subsequent calls will execute 'kubectl' directly
unfunction "$0"

# Load auto-completion
source <(kubectl completion zsh)

# Execute 'kubectl' binary
$0 "$@"
}
fi

If you want to build the same using bash, the commands are a little bit different:

#!/usr/bin/env bash

if type kubectl &>/dev/null; then
kubectl() {
unset -f "$0"
source <(kubectl completion zsh)
$0 "$@"
}
fi

With this lazy-loading technique, my zsh startup time went down from 2085ms to 373ms!