My ZSH config built for speed

A performance-optimized ZSH configuration that achieves 30ms startup times while loading 11+ plugins including syntax highlighting, autosuggestions, and intelligent completions.

0 views
--- likes

Over the years I’ve tuned my ZSH setup to be fast first without losing the developer features I rely on. The idea is simple: keep the prompt responsive, then load extras in the background. I separate environment setup from interactive features and defer non‑essential plugins, so you never trade functionality for speed.

I use Sheldon (a lightweight plugin manager) and zsh‑defer to load 11+ plugins without blocking startup, and I store history in iCloud for seamless cross‑Mac sync.

In this post I share the full config so you can get the same fast, feature‑rich shell.

The result: an immediately responsive Zsh with syntax highlighting, smart autosuggestions, fuzzy finding, and rich git integration—without lag. Here’s a quick benchmark booting Zsh a few times:

 for i in $(seq 1 5); do /usr/bin/time zsh -i -c exit; done
        0.03 real         0.01 user         0.01 sys
        0.03 real         0.01 user         0.01 sys
        0.03 real         0.01 user         0.01 sys
        0.03 real         0.01 user         0.01 sys
        0.03 real         0.01 user         0.01 sys

As you can see from the results we consistently hit 30ms startup times while loading syntax highlighting, autosuggestions, intelligent completions, and git integration with the use of strategic deferred loading.

Prerequisites

Install the tools used by previews and plugin setup:

brew install sheldon fzf eza bat fd git-delta jaq tldr

ZSH Startup File Execution Order

Understanding when ZSH loads each configuration file is crucial for optimizing performance:

  1. .zshenv - Always loaded first, for every ZSH invocation (interactive, non-interactive, login, non-login)
  2. .zprofile - Loaded for login shells only (when you first open Terminal or SSH into a machine)
  3. .zshrc - Loaded for interactive shells (when you can type commands)

This execution order allows us to:

  • Put essential environment setup in .zshenv
  • Configure paths and heavy environment variables in .zprofile (loaded less frequently)
  • Keep interactive features in .zshrc for responsive shell startup

Configuration Files

.zshenv

This file ensures non-login shells have access to the profile configuration. The key principle here is minimalism—since .zshenv loads for every ZSH invocation (including non-interactive scripts), we keep it extremely lightweight to avoid slowing down background processes.

By conditionally sourcing .zprofile only when needed, we ensure that heavyweight environment setup (paths, Homebrew configuration) is available to subshells without duplicating the expensive initialization work:

#----------------------
# .zshenv - Zsh environment file, loaded always.
#----------------------
# Source: https://github.com/sorin-ionescu/prezto/blob/master/runcoms/zshenv
 
# Ensure that a non-login, non-interactive shell has a defined environment.
if [[ ("$SHLVL" -eq 1 && ! -o LOGIN) && -s "${ZDOTDIR:-$HOME}/.zprofile" ]]; then
    source "${ZDOTDIR:-$HOME}/.zprofile"
fi

.zprofile

This is the one‑time, per‑login heavyweight setup that won’t slow down every new tab. It adopts XDG base dirs to keep $HOME clean, detects Apple Silicon vs. Intel to point Homebrew correctly, and flips performance dials like HOMEBREW_NO_AUTO_UPDATE and HOMEBREW_NO_ANALYTICS so brew runs quicker and quieter.

PATH is built predictably (Coreutils’ gnubin first, deduped with typeset -U) for consistent speed across machines. It also primes power tools for features without latency: fzf uses fd for blazing file search, man pages render via bat, and editor/pager defaults are set for a smooth, modern CLI.

#----------------------
# .zprofile path setup
#----------------------
# It's best practice to setup path in .zprofile
# source: https://mac.install.guide/terminal/zshrc-zprofile
 
#----------------------
# XDG Base Directory Specification
#----------------------
typeset -x XDG_CACHE_HOME="$HOME/.cache"
typeset -x XDG_CONFIG_HOME="$HOME/.config"
typeset -x XDG_DATA_HOME="$HOME/.local/share"
typeset -x XDG_STATE_HOME="$HOME/.local/state"
 
#----------------------
# Homebrew Configuration
#----------------------
# Determine architecture and set Homebrew paths
if [[ $CPUTYPE == arm64 ]]; then
    typeset -x HOMEBREW_PREFIX="/opt/homebrew"
    typeset -x HOMEBREW_REPOSITORY="$HOMEBREW_PREFIX"
else
    typeset -x HOMEBREW_PREFIX="/usr/local"
    typeset -x HOMEBREW_REPOSITORY="$HOMEBREW_PREFIX/Homebrew"
fi
 
# Homebrew Performance Optimizations
typeset -x HOMEBREW_CELLAR="$HOMEBREW_PREFIX/Cellar" \
    HOMEBREW_BAT=1 \
    HOMEBREW_BUNDLE_INSTALL_CLEANUP=1 \
    HOMEBREW_CASK_OPTS="--no-quarantine" \
    HOMEBREW_CLEANUP_MAX_AGE_DAYS=7 \
    HOMEBREW_CLEANUP_PERIODIC_FULL_DAYS=5 \
    HOMEBREW_NO_ANALYTICS=1 \
    HOMEBREW_NO_AUTO_UPDATE=1 \
    HOMEBREW_NO_COLOR=1 \
    HOMEBREW_NO_EMOJI=1 \
    HOMEBREW_NO_ENV_HINTS=1 \
    HOMEBREW_NO_INSECURE_REDIRECT=1 \
    HOMEBREW_UPGRADE_GREEDY=1
 
#----------------------
# Path Configuration
#----------------------
# LATEST_PYTHON_PATH=${${(Oa)$(echo $HOME/.local/share/uv/python/cpython-3*-macos-*-none)}[1]}
# $LATEST_PYTHON_PATH/bin(N) if I want to use the uv version of python
typeset -U path fpath
path=(
    $HOMEBREW_PREFIX/opt/{python@3/libexec,node@20}/bin(N)
    $HOMEBREW_PREFIX/opt/coreutils/libexec/gnubin(N)
    $HOMEBREW_PREFIX/{bin,sbin}(N)
    $HOME/{go/bin,.orbstack/bin,.local/bin}(N)
    $path
)
 
fpath=(
    # OrbStack: command-line tools and integration is automatically setup in .zprofile
    /Applications/OrbStack.app/Contents/{MacOS,Resources}/completions/zsh(N)
    $HOME/.granted/zsh_autocomplete/assume(N)
    $fpath
)
 
# Ensure path arrays are unique and exported
typeset -x -U PATH path FPATH fpath MANPATH manpath
typeset -x -UT INFOPATH infopath
 
#----------------------
# System Configuration
#----------------------
# Locale settings
typeset -x LANG=${LANG:-'en_US.UTF-8'}
typeset -x LC_ALL=${LC_ALL:-'en_US.UTF-8'}
 
# System utilities
typeset -x EDITOR=code \
    VISUAL=code \
    PAGER=less \
    BROWSER='open' \
    GPG_TTY=$TTY \
    SHELL_SESSIONS_DISABLE=1 \
    LESSOPEN="|$HOME/.lesspipe %s" \
    MANPAGER="col -xbf | bat -p -l man"
 
#----------------------
# Development Tools
#----------------------
# FZF Configuration
typeset -x FZF_DEFAULT_COMMAND='fd --type file --follow --hidden --exclude .git --exclude node_modules'
typeset -x FZF_DEFAULT_OPTS="
--height 35%
--layout=reverse
--border
--cycle
--multi
--info=inline
"
 
typeset -x FZF_CTRL_T_COMMAND="$FZF_DEFAULT_COMMAND"
typeset -x FZF_CTRL_T_OPTS="
--height=90%
--preview-window=right:65%
--prompt='∼ ' --pointer='▶' --marker='✓'
--preview '([[ -f {} ]] && bat --style=numbers,header,grid --color=always {}) ||
          ([[ -d {} ]] && (eza --tree -I node_modules --git-ignore {} | less)) ||
          echo {} 2> /dev/null | head -200'
--bind '?:toggle-preview,ctrl-a:select-all,ctrl-y:execute-silent(echo {+} | pbcopy),ctrl-v:execute(code {+})'
--bind 'tab:toggle+down,shift-tab:toggle+up'
"
 
typeset -x FZF_CTRL_R_OPTS="--prompt='History> '"
 
#------------------
# zsh-users/zsh-autosuggestions
#------------------
typeset -g ZSH_AUTOSUGGEST_BUFFER_MAX_SIZE=30
typeset -g ZSH_AUTOSUGGEST_HIGHLIGHT_STYLE='fg=240'
typeset -g ZSH_AUTOSUGGEST_MANUAL_REBIND=true
typeset -g ZSH_AUTOSUGGEST_STRATEGY=(history completion)
 
#------------------
# zsh-users/zsh-history-substring-search
#------------------
typeset -g HISTORY_SUBSTRING_SEARCH_ENSURE_UNIQUE=true
typeset -g HISTORY_SUBSTRING_SEARCH_HIGHLIGHT_FOUND='fg=#00cc00,bold'     #p10k green
typeset -g HISTORY_SUBSTRING_SEARCH_HIGHLIGHT_NOT_FOUND='fg=#FF5C57,bold' #p10k red

.zshrc

Here’s where speed meets daily ergonomics. History is stored in iCloud so it follows you across Macs, with aggressive de‑duplication and INC_APPEND_HISTORY/SHARE_HISTORY for fast, reliable search. Plugins load via Sheldon with most UI niceties deferred, so your prompt is usable immediately.

The completion system is tuned for performance (caching, accept‑exact matches, mtime sorting) and upgraded with fzf-tab, turning Tab into an interactive finder with rich previews: eza for directories, delta for git diffs, jaq‑powered package details for Homebrew, plus smart process/env previews.

You get context‑aware, discoverable completions without paying a startup‑time tax.

#---------------
# .zshrc setup
#---------------
# Always set these first, so history is preserved, no matter what happens.
# On macOS, store it in iCloud, so it syncs across multiple Macs.
HISTFILE="${HOME}/Library/Mobile Documents/com~apple~CloudDocs/.zsh_history"
 
# Max number of history entries to keep in memory.
# 1.2 * SAVEHIST is Zsh recommended value for HISTSIZE
HISTSIZE=14400           # Maximum events in internal history
SAVEHIST=12000           # Maximum events in history file
HIST_STAMPS="dd-mm-yyyy" # Add timestamps to history
 
# Commands to ignore in history (extended pattern matching)
HISTORY_IGNORE="(ls|ll|la|cd|pwd|exit|clear|history|top|htop|:q|(rm|cp|mv|less|more|vim|code|which|man|df|du|free|ps|kill) *)"
 
# Set options for history
setopt EXTENDED_HISTORY HIST_EXPIRE_DUPS_FIRST HIST_FCNTL_LOCK HIST_FIND_NO_DUPS \
    HIST_IGNORE_ALL_DUPS HIST_IGNORE_DUPS HIST_IGNORE_SPACE HIST_NO_FUNCTIONS \
    HIST_NO_STORE HIST_REDUCE_BLANKS HIST_SAVE_NO_DUPS HIST_VERIFY \
    INC_APPEND_HISTORY SHARE_HISTORY
unsetopt HIST_BEEP
 
#------------------
# Load ZSH plugins
#------------------
builtin source <(sheldon source)
 
#------------------
# Zstyle configurations
# Remember to use single quotes here!
# Source for zstyles: https://github.com/fluxninja/dotfiles/blob/master/dot_zshrc
#------------------
# Basic completion settings
zstyle ':completion:*' completer _complete _match _approximate
zstyle ':completion:*' verbose true
zstyle ':completion:*' matcher-list 'm:{a-zA-Z}={A-Za-z}' 'r:|[._-]=* r:|=*' 'l:|=* r:|=*'
zstyle ':completion:*' special-dirs true
 
# Performance optimizations
zstyle ':completion:*' accept-exact '*(N)'
zstyle ':completion:*' complete-options true
zstyle ':completion:*' file-sort modification
zstyle ':completion:*' use-cache on
zstyle ':completion:*' cache-path "$XDG_CACHE_HOME/.zcompcache"
 
# Menu interface improvements
zstyle ':completion:*' menu select
zstyle ':completion:*:descriptions' format '-- %d --'
zstyle ':completion:*:messages' format '%F{purple}-- %d --%f'
zstyle ':completion:*:warnings' format '%F{red}-- no matches found --%f'
 
# Ignore certain functions
zstyle ':completion:*:functions' ignored-patterns '(_*|pre(cmd|exec))'
 
# Git-specific settings
zstyle ':completion:*:git-checkout:*' sort false
 
# FZF-tab configurations
zstyle ':fzf-tab:*' fzf-bindings 'space:accept'
zstyle ':fzf-tab:*' switch-group ',' '.'
 
# Process completion (macOS compatible)
zstyle ':completion:*:*:*:*:processes' command "ps -u $USER -o pid,user,comm"
zstyle ':fzf-tab:complete:(kill):argument-rest' fzf-preview \
    '[[ $group == "[process ID]" ]] && ps -p $word -o comm='
zstyle ':fzf-tab:complete:(kill):argument-rest' fzf-flags --preview-window=down:10:wrap
 
# Directory and file preview with eza
zstyle ':fzf-tab:complete:cd:*' fzf-preview 'eza -1lh --icons --git-ignore --group-directories-first --sort=accessed --color=always $realpath'
zstyle ':fzf-tab:complete:cd:*' fzf-flags --height=35% --preview-window=right:65%
 
# Environment variables preview
zstyle ':fzf-tab:complete:(-parameter-|-brace-parameter-|export|unset|expand):*' \
    fzf-preview 'echo ${(P)word}'
zstyle ':fzf-tab:complete:(-parameter-|-brace-parameter-|export|unset|expand):*' \
    fzf-flags --preview-window=down:10:wrap
 
# Homebrew completions
zstyle ':fzf-tab:complete:brew-(install|uninstall|search|info):*-argument-rest' \
    fzf-preview '
    package_type="formula"
    if [[ $group == *"cask"* ]]; then
        package_type="cask"
    fi
 
    if [[ $package_type == "formula" ]]; then
        jaq -r --arg name "$word" '\''
        .formulae[] |
        select(.name == $name) |
        "Name: \(.name)\nDescription: \(.desc)\nHomepage: \(.homepage)\nInstalled version: \(if .installed and (.installed | length > 0) then .installed[0].version else "Not installed" end)\nUpstream version: \(.versions.stable // "Unknown")\nDependencies: \(if .dependencies then (.dependencies | join(", ")) else "None" end)"
        '\'' "$XDG_CACHE_HOME/brew_info.json"
    else
        jaq -r --arg name "$word" '\''
        .casks[] |
        select(.token == $name) |
        "Name: \(.token)\nDescription: \(.desc)\nHomepage: \(.homepage)\nVersion: \(.version)\nInstalled: \(if .installed == "true" then "Yes" else "No" end)"
        '\'' "$XDG_CACHE_HOME/brew_info.json"
    fi
    '
zstyle ':fzf-tab:complete:brew*:*' fzf-flags --height=45% --preview-window=right:65%:wrap
 
# Show tldr and man info in preview section
zstyle ':fzf-tab:complete:(\\|*/|)man:*' fzf-preview 'man $word'
zstyle ':fzf-tab:complete:tldr:argument-1' fzf-preview 'tldr --color always $word'
 
# Show tldr info of cli commands in preview section
zstyle ':fzf-tab:complete:-command-:*' fzf-preview \
    '(out=$(tldr --color always "$word" 2>/dev/null) && echo "$out" || \
    out=$(MANWIDTH=$FZF_PREVIEW_COLUMNS man "$word" 2>/dev/null) && echo "$out" || \
    out=$(which "$word") && echo "$out" || echo "${(P)word}")'
zstyle ':fzf-tab:complete:-command-:*' fzf-flags --height=60% --preview-window=right:65%:wrap
 
# Use .lesspipe to preview content files, directories etc.
zstyle ':fzf-tab:complete:(cat|bat|code|vim):argument-rest' fzf-preview 'less ${(Q)realpath}'
zstyle ':fzf-tab:complete:(cat|bat|code|vim):argument-rest' fzf-flags --height=75% --preview-window=right:75%
zstyle ':fzf-tab:complete:(eza|ls|fd|find|cp|mv|rm):argument-rest' fzf-preview 'less ${(Q)realpath}'
zstyle ':fzf-tab:complete:(eza|ls|fd|find|cp|mv|rm):argument-rest' fzf-flags --height=45% --preview-window=right:65%
 
# Preview git -- these don't work with homebrew git package
zstyle ':fzf-tab:complete:git-(add|diff|restore):*' fzf-preview 'git diff $word | delta'
zstyle ':fzf-tab:complete:git-log:*' fzf-preview 'git log --color=always $word'
zstyle ':fzf-tab:complete:git-help:*' fzf-preview 'git help $word | bat -plman --color=always'
zstyle ':fzf-tab:complete:git-show:*' fzf-preview \
    'case "$group" in
    "commit tag") git show --color=always $word ;;
    *) git show --color=always $word | delta ;;
   esac'
zstyle ':fzf-tab:complete:git-checkout:*' fzf-preview \
    'case "$group" in
    "modified file") git diff $word | delta ;;
    "recent commit object name") git show --color=always $word | delta ;;
    *) git log --color=always $word ;;
   esac'
zstyle ':fzf-tab:complete:git-*:*' fzf-flags --height=85% --preview-window=right:75%:wrap

Sheldon Plugin Manager

Instead of Oh My Zsh or similar frameworks, I use Sheldon for its speed and precise loading control.

Sheldon's philosophy aligns perfectly with our performance-first approach—it's a minimal, fast plugin manager written in Rust that gives you granular control over when and how plugins load.

Unlike monolithic frameworks that bundle everything together, Sheldon lets you cherry-pick exactly the plugins you need and control their loading sequence.

The following setup provides a fully-featured development environment with 11+ carefully selected plugins covering syntax highlighting, intelligent autosuggestions, fuzzy finding, git integration, Python virtual environment management, enhanced completions, and modern file navigation—all while maintaining our sub-30ms startup times.

The key is strategically organizing plugins by loading priority in ~/.config/sheldon/plugins.toml:

# `sheldon` configuration file
# ----------------------------
#
# You can modify this file directly or you can use one of the following
# `sheldon` commands which are provided to assist in editing the config file:
#
# - `sheldon add` to add a new plugin to the config file
# - `sheldon edit` to open up the config file in the default editor
# - `sheldon remove` to remove a plugin from the config file
#
# See the documentation for more https://github.com/rossmacarthur/sheldon#readme
#
# Inspired by https://github.com/rossmacarthur/dotfiles/blob/trunk/src/sheldon/plugins.toml
#
# Load order of zsh-users plugins:
# https://www.reddit.com/r/zsh/comments/1etl9mz/comment/lie04dt/?utm_source=share&utm_medium=web3x&utm_name=web3xcss&utm_term=1&utm_content=share_button
# https://github.com/romkatv/zsh-bench?tab=readme-ov-file#deferred-initialization
 
shell = "zsh"
 
[templates]
defer = """{{ hooks?.pre | nl }}{% for file in files %}zsh-defer source "{{ file }}"\n{% endfor %}{{ hooks?.post | nl }}"""
defer-more = """{{ hooks?.pre | nl }}{% for file in files %}zsh-defer -t 0.5 source "{{ file }}"\n{% endfor %}{{ hooks?.post | nl }}"""
fpath = 'fpath=( "{{ dir }}" $fpath )'
path = 'path=( "{{ dir }}" $path )'
 
# Paths
# -----
 
[plugins.git-open]
github = "paulirish/git-open"
apply = ["path"]
 
[plugins.zsh-bench]
github = "romkatv/zsh-bench"
apply = ["path"]
 
# Completions
# -----------
 
[plugins.zsh-completions]
github = "zsh-users/zsh-completions"
use = ["{{ name }}.plugin.zsh"]
 
[plugins.fzf]
github = "junegunn/fzf"
use = ["{completion,key-bindings}.zsh"]
 
# Sourced
# -------
 
[plugins]
[plugins.zsh-defer]
github = "romkatv/zsh-defer"
 
[plugins.zsh-autoswitch-virtualenv]
github = "dannysteenman/zsh-autoswitch-virtualenv"
use = ["autoswitch_virtualenv.plugin.zsh"]
 
[plugins.powerlevel10k]
github = "romkatv/powerlevel10k"
hooks.post = "source ~/.config/zsh/themes/.p10k.zsh"
 
[plugins.compinit]
inline = "autoload -Uz compinit && zsh-defer compinit"
 
# Deferred plugins
# ----------------
 
# fzf-tab needs to be loaded after compinit, but before plugins which will wrap widgets,
# such as zsh-autosuggestions or fast-syntax-highlighting
[plugins.fzf-tab]
github = "Aloxaf/fzf-tab"
use = ["{{ name }}.plugin.zsh"]
apply = ["defer"]
 
[plugins.fast-syntax-highlighting]
github = "zdharma-continuum/fast-syntax-highlighting"
use = ["{{ name }}.plugin.zsh"]
apply = ["defer"]
 
# Plugins that are even more deferred
# -----------------------------------
 
# This should be loaded AFTER zsh-syntax-highlighting.
[plugins.zsh-history-substring-search]
github = "zsh-users/zsh-history-substring-search"
use = ["{{ name }}.zsh"]
apply = ["defer-more"]
hooks.post = "bindkey '^[[A' history-substring-search-up; bindkey '^[[B' history-substring-search-down"
 
[plugins.zsh-autosuggestions]
github = "zsh-users/zsh-autosuggestions"
use = ["{{ name }}.zsh"]
apply = ["defer-more"]

As a follow-up to the config above: you don’t need a bloated framework like Oh My Zsh to get a fully‑featured shell. These 11 plugins are my daily drivers—fast, low‑overhead, and purpose‑built. Here’s a quick rundown of what they are and what they unlock:

Core Performance & Management:

  • zsh-defer - The foundation that enables our fast startup times by deferring plugin loading
  • Powerlevel10k - Lightning-fast prompt with git integration and customizable segments (see my P10k configuration)

Enhanced Navigation & Search:

  • fzf - Fuzzy finding for files, history, and command-line workflows
  • fzf-tab - Transforms tab completion into an interactive fuzzy finder with rich previews

Intelligent Input & Completion:

Syntax & Development:

Development Tools:

  • git-open - Quickly open git repositories in your browser
  • zsh-bench - Performance benchmarking tool for measuring shell startup times

Full Performance Benchmark

Using zsh-bench for comprehensive benchmarking:

==> benchmarking login shell of user dsteenman ...
creates_tty=0
has_compsys=0
has_syntax_highlighting=0
has_autosuggestions=0
has_git_prompt=1
first_prompt_lag_ms=20.749
first_command_lag_ms=95.359
command_lag_ms=2.251
input_lag_ms=2.608
exit_time_ms=39.472

These results show an interactive performance with ~21ms to first prompt and minimal input lag of just 2.6ms, ensuring a responsive shell experience even with a feature-rich configuration loading 11+ plugins.

The key to this performance is the strategic use of zsh-defer from Roman Perepelitsa (author of Powerlevel10k), which allows the shell to become interactive immediately while loading plugins in the background. This approach provides the best of both worlds: instant responsiveness with full feature availability within milliseconds.

The strategic loading order ensures that essential functionality (prompt, completions) loads immediately, while visual enhancements (syntax highlighting, autosuggestions) load in the background without blocking shell interaction. This approach delivers a terminal experience that's both lightning-fast and feature-rich, perfect for intensive development workflows.