Fix slow zsh startup in Git Bash on Windows. Why Oh My Zsh and Powerlevel10k take over a second to load under MSYS2, how to measure it, and safe fixes that cut zsh startup from 719 ms to 243 ms without losing a single plugin.
Keywords: oh my zsh slow startup windows, git bash zsh slow, powerlevel10k git bash windows, msys2 zsh startup time, zsh-defer, compinit slow, zprof, zsh profiling
⚠️ Warning: AI can make mistakes. Everything here changes your shell startup files (~/.zshrc,~/.bashrc, your Oh My Zsh install). One wrong line can leave you without a working terminal. Back up first, read every change before it is applied, and keep a second terminal (for example PowerShell) open so you can roll back. If you use the AI prompts at the end, supervise every step and approve each change yourself.
Opening a new Git Bash tab on Windows took about 1.1 seconds before you could type, even though a bare zsh starts in under 70 ms. The cause was not one slow plugin. On Windows, every external program a shell script starts costs about 20 ms, because MSYS2 has to emulate Unix fork() (and the antivirus inspects each launch). A stock Oh My Zsh + Powerlevel10k setup launched around 30 of them on every startup. On top of that came a duplicated compinit, a slow completion permission audit, plugins that ran before the first prompt, and stale lock folders left behind by killed shells.
check: https://gist.github.com/fworks/af4c896c9de47d827d4caa6fd7154b6b (fworks/install-zsh-windows-git-bash.md)
- Windows 11, Windows Terminal and VS Code's integrated terminal
- Git for Windows (Git Bash), with MSYS2's zsh 5.9 copied into Git's
/usr ~/.bashrchands over to zsh:if [ -t 1 ]; then exec zsh; fi- Oh My Zsh + Powerlevel10k (instant prompt enabled)
- Plugins:
git,zsh-autosuggestions,zsh-syntax-highlighting,composer,artisan
| Measurement (median of 7 runs) | Before | After |
|---|---|---|
zsh startup: zsh -i -c exit |
719 ms | 243 ms (−66%) |
Full chain: bash -l -i -c 'exec zsh -i -c exit' |
1071 ms | 648 ms (−40%) |
Your numbers will differ. Measure your own machine first.
# Bare zsh vs. your config (repeat a few times and compare)
time zsh -f -c exit
time zsh -i -c exit
# Cost of one process launch on your machine
zsh -f -c 'zmodload zsh/datetime; t=$EPOCHREALTIME; repeat 100 command true; print "$(( (EPOCHREALTIME-t)*10 )) ms per launch"'To see which functions are slow, temporarily put zmodload zsh/zprof on the first line of ~/.zshrc and zprof on the last.
- Don't profile by pointing
ZDOTDIRat another folder. Oh My Zsh then misses its completion cache, and you end up measuring a cold 6–8 scompinit. zsh -i -c exithas no real terminal, so Powerlevel10k's instant prompt and Oh My Zsh's update check don't run there. Always confirm in a real tab.
- Update Oh My Zsh first:
omz update. Recent versions read git metadata and the completion cache directly, instead of startinggitandgrepon every launch. - Remove the duplicate
compinit. Oh My Zsh already runs it, so a second call at the end of.zshrcre-scans every completion and re-runs the audit (~100 ms). If you added it for a custom completion folder, add that folder tofpathbefore Oh My Zsh loads instead. ZSH_DISABLE_COMPFIX=true. Oh My Zsh then usescompinit -uand skips its insecure-folder filtering and warnings. On NTFS, Unix permissions are made up by MSYS2, so that check rarely means anything. Trade-off: you won't be warned if a completion folder becomes writable by other users.- Clean up PATH:
typeset -U path fpathandpath=($^path(N-/)). Windows PATH is long and full of duplicates and dead folders. zsh scans every folder the first time it looks up commands. - Skip Powerlevel10k's SSH probe when you're not on SSH. p10k starts
whojust to detect SSH sessions, and a local terminal doesn't need that. - Turn off automatic update checks:
zstyle ':omz:update' mode disabled. When a check is due, it can contact GitHub and stop at a[Y/n]prompt during startup. Closing the tab there leaves a lock behind. Runomz updateyourself every few weeks. - Remove stale lock folders. A shell killed mid-startup can leave
~/.zcompdump-*.lockor$ZSH/log/update.lockbehind. The first one silently stops Oh My Zsh from ever recompiling its completion cache. Remove them withrmdir, which deletes empty folders only, and only while no other shell is starting. - Load non-essential plugins after the prompt with zsh-defer. Autosuggestions, syntax highlighting and completion-only plugins don't need to exist before your first prompt, so zsh-defer loads them once the shell is idle. Powerlevel10k's instant prompt hides the delay, but your keystrokes still wait for startup to finish.
- Keep third-party plugins in
$ZSH_CUSTOM/plugins, never in$ZSH/plugins. Oh My Zsh's.gitignoreexcludescustom/, soomz updatenever touches your plugins or these changes.
After changing plugins or fpath, the first new shell rebuilds the completion cache once, which takes a few seconds. Run zsh -i -c exit once to get it over with.
# (Powerlevel10k instant prompt block stays at the very top)
export ZSH="$HOME/.oh-my-zsh"
# Startup speed: on Windows/MSYS every forked process costs ~20ms.
ZSH_DISABLE_COMPFIX=true # no insecure-dir filtering/warning (NTFS perms are synthetic)
typeset -U path fpath # drop duplicate PATH entries, first one wins
path=($^path(N-/)) # drop PATH dirs that don't exist
# p10k forks `who` to detect SSH; only needed when actually logged in over SSH
[[ -z $SSH_CONNECTION$SSH_CLIENT$SSH_TTY ]] && typeset -gx P9K_SSH=0 _P9K_SSH_TTY=$TTY
ZSH_THEME="powerlevel10k/powerlevel10k"
zstyle ':omz:update' mode disabled # run `omz update` manually
plugins=(git) # the rest are deferred, see the end of the file
fpath+=~/.zfunc # custom completions go on fpath BEFORE Oh My Zsh runs compinit
source $ZSH/oh-my-zsh.sh
[[ ! -f ~/.p10k.zsh ]] || source ~/.p10k.zsh
# Load non-essential plugins once the prompt is idle.
source $ZSH_CUSTOM/plugins/zsh-defer/zsh-defer.plugin.zsh
zsh-defer source $ZSH/plugins/composer/composer.plugin.zsh
zsh-defer source $ZSH_CUSTOM/plugins/artisan/artisan.plugin.zsh
zsh-defer source $ZSH_CUSTOM/plugins/zsh-autosuggestions/zsh-autosuggestions.zsh
zsh-defer source $ZSH_CUSTOM/plugins/zsh-syntax-highlighting/zsh-syntax-highlighting.zsh # keep lastInstall zsh-defer:
git clone https://github.com/romkatv/zsh-defer "$ZSH_CUSTOM/plugins/zsh-defer"Check the deferred plugins in a real terminal tab (wait a second after it opens). It should print 1 1:
print $+functions[_zsh_autosuggest_start] $+functions[_zsh_highlight]Rollback: back up first (cp ~/.zshrc ~/.zshrc.bak), and restore with cp ~/.zshrc.bak ~/.zshrc.
Still left on the table: Git for Windows' /etc/profile costs about 300 ms before zsh even starts, because ~/.bashrc runs exec zsh only after it. Trimming that means changing how your terminal launches the shell, which is out of scope here.
Run the first prompt, review its report, then run the second one in the same session. Supervise every step. These prompts touch the files your terminal needs in order to start.
You are diagnosing slow shell startup on my machine. This is READ-ONLY: do not edit, create, move or delete any file outside a temporary folder, and do not run updates or installers.
My setup: Git Bash on Windows (MSYS2) handing over to zsh with Oh My Zsh (and possibly Powerlevel10k). Work fastest-first and show me each command before you run it:
1. Map the startup chain: the command my terminal launches (Windows Terminal settings.json, VS Code terminal profile), what ~/.bashrc and ~/.bash_profile do, $MSYSTEM, and the zsh, Git for Windows and Oh My Zsh versions.
2. Time it, 7 runs each, and report medians: `zsh -f -c exit` (bare), `zsh -i -c exit` (my config), `bash -l -i -c "exec zsh -i -c exit"` (full chain).
3. Measure the cost of one process launch: time `repeat 100 command true` with $EPOCHREALTIME inside `zsh -f`. Also time `git --version`.
4. Profile ~/.zshrc with zprof WITHOUT editing it: make a temp folder whose .zshrc is `ZDOTDIR=$HOME; zmodload zsh/zprof; source ~/.zshrc; zprof`, then run `ZDOTDIR=<that folder> zsh -i -c exit`. The `ZDOTDIR=$HOME` line matters: without it Oh My Zsh misses its completion cache and you measure a cold 6-8 s compinit.
5. Check the known culprits: how many times compinit runs; ZSH_DISABLE_COMPFIX; the plugin list and which plugins start external commands at load; PATH entry count, duplicates and missing folders; stale `~/.zcompdump*.lock` and `$ZSH/log/update.lock` folders; the Oh My Zsh update mode and how many commits it is behind origin; untracked folders inside $ZSH (plugins cloned into the wrong place); whether Powerlevel10k instant prompt is on.
6. Count the external commands started during startup. xtrace with a timestamped PS4 is fine for counting, but its per-line timings are heavily inflated on Windows, so don't trust them.
Remember that `zsh -i -c exit` has no TTY, so Powerlevel10k instant prompt and the Oh My Zsh update check don't run there. List anything you could not measure.
Output: baseline numbers, a table of where the time goes (ms, cause, evidence), and a ranked list of fixes with expected savings and trade-offs. Do not apply anything.Using your diagnosis report above, fix my slow zsh startup (Git Bash on Windows + Oh My Zsh). Rules:
- Ask me before EVERY change: show the exact diff or command and wait for my OK. These are shell startup files, and a mistake can break my terminal.
- First back up ~/.zshrc (and ~/.bashrc if you will touch it) to dated copies, and tell me the rollback command.
- Never edit Oh My Zsh's own files. Every third-party plugin must live in $ZSH_CUSTOM/plugins, because Oh My Zsh's .gitignore excludes custom/ and `omz update` never touches it. If plugins were cloned into $ZSH/plugins, move them to $ZSH_CUSTOM/plugins.
- Do not change antivirus or security settings.
Apply only the fixes your report justified, one at a time, in this order:
1. `omz update` (newer versions start fewer processes). Check that `git status` in $ZSH is clean first.
2. Remove any second compinit, and put custom completion folders on fpath before `source $ZSH/oh-my-zsh.sh`.
3. `ZSH_DISABLE_COMPFIX=true` before Oh My Zsh loads (explain the trade-off to me).
4. `typeset -U path fpath` and `path=($^path(N-/))` before Oh My Zsh loads.
5. Only if Powerlevel10k is used: `[[ -z $SSH_CONNECTION$SSH_CLIENT$SSH_TTY ]] && typeset -gx P9K_SSH=0 _P9K_SSH_TTY=$TTY` to skip its `who` probe.
6. `zstyle ':omz:update' mode disabled` (I will run `omz update` myself).
7. Remove stale lock folders with `rmdir` (empty folders only), and only if no other shell is starting.
8. Install romkatv/zsh-defer into $ZSH_CUSTOM/plugins/zsh-defer. Remove zsh-autosuggestions, zsh-syntax-highlighting and other non-essential plugins from `plugins=()`, and load them at the end of ~/.zshrc with `zsh-defer source <plugin file>`, with zsh-syntax-highlighting last. Keep the Powerlevel10k instant prompt block at the top.
After each step, run `zsh -n ~/.zshrc`, then a fresh `zsh -i -c exit`, and check for errors. After a plugin or fpath change, run it once more so the completion cache rebuilds (this takes a few seconds, once).
At the end, prove nothing was lost: completions load (`$+_comps[git]`), git aliases exist, the theme loads, PATH has no duplicates, and the deferred plugins load. Deferred plugins never run under `zsh -i -c`, so replay the queue: `for t in $_zsh_defer_tasks; do _zsh-defer-apply "${t#* }"; done`, then check their functions exist. Re-run the prompt-1 timings (7 runs, medians) and show a before/after table. Finally, remind me to confirm in a real terminal tab.