Bash, Zsh, and Fish compared for 2026: startup time, script compatibility, and why you should type in one shell and script in Bash.
Your onboarding script installs Oh My Zsh with twelve plugins because that is what the last engineer set up. Six months later every new terminal tab takes over a second to open, and nobody remembers why. Meanwhile, the CI runner executing your deploy script has no Zsh at all. It only has /bin/sh, and the script fails on syntax Zsh accepted without complaint.
That is the real shape of the Bash vs Zsh vs Fish decision in 2026. It is not one choice but two separate ones: what you use to type commands, and what you write scripts in. Conflating them is where most of the pain comes from.
Bash is the portable default, Zsh is a Bash-compatible shell with a rich interactive layer, and Fish is a friendly interactive shell that deliberately breaks POSIX compatibility. Each optimises for a different job, and that is the right lens before comparing features line by line.
No. A bare install of any of the three starts in well under a quarter of a second on modern hardware, and the gap between them has closed. The slowness people blame on Zsh is almost always an unmanaged plugin pile, and that is fixable without switching shells.
Measure instead of guessing. hyperfine runs each command many times and reports the mean:
## Compare interactive startup across shellshyperfine --warmup 3 \ 'bash -i -c exit' \ 'zsh -i -c exit' \ 'fish -c exit'If Zsh is the slow one, profile it. Add zprof to the config, open a new shell, and read which function eats the time:
## First line of ~/.zshrczmodload zsh/zprof ## Last line of ~/.zshrczprofThe usual offenders are version managers such as nvm loading in .zshrc on every start, and a long list of Oh My Zsh plugins. Lazy-load the managers, keep three or four plugins, or switch to a lighter manager such as antidote or sheldon. Fish has a built-in equivalent in fish --profile-startup=/tmp/fish.prof -ic exit.
Only Bash (or POSIX sh) syntax belongs in committed scripts. Zsh can often source Bash files, but Zsh scripts break on any host without Zsh, and Fish scripts break on essentially every server, container, and CI runner you will meet.
## Bash and Zsh: export a variableexport AWS_REGION=ap-south-1 ## Fish: different syntax, not interchangeableset -gx AWS_REGION ap-south-1The detail most comparisons undersell is that your interactive shell and your scripting language are independent decisions. You can type in Fish all day and still write every deploy script under this shebang:
#!/usr/bin/env bashset -euo pipefailThat shebang guarantees the script runs the same way on your laptop, in CI, and on a production server that has never heard of Fish. Add shellcheck to your pipeline and it will flag most portability mistakes before they ship.
Zsh looks like Bash until it does not, and the differences fail silently. Two of them cause most of the damage:
${arr[0]} is empty in Zsh.files="a.txt b.txt" ## Bash prints two lines, Zsh prints onefor f in $files; do echo "$f"doneIf your daily shell is Zsh, test scripts with bash script.sh and not by sourcing them, so you see what the CI runner will see.
Not by default. macOS still ships Bash 3.2 at /bin/bash for licensing reasons, so features like declare -A associative arrays and mapfile fail on a stock Mac. Install a current Bash with brew install bash, and note that #!/usr/bin/env bash will pick it up only if Homebrew's bin comes first in your PATH. For scripts shared across macOS and Linux, either avoid Bash 4 features or document the requirement.
Fish wins the out-of-the-box experience. Autosuggestions, syntax highlighting, and tab completions with descriptions work immediately after installing, with no config file. Zsh reaches similar features through plugins, and in return you get a larger ecosystem.
## ~/.zshrc: the same features need explicit setupplugins=(git zsh-autosuggestions zsh-syntax-highlighting)source $ZSH/oh-my-zsh.shCompletion quality differs by tool. Zsh completions for kubectl, helm, aws, and docker are largely hand-written and frequently sharper, while Fish generates many completions by parsing man pages and ships its own for popular tools. In practice both work well, and the gap shows mainly with newer or niche CLIs.
š” Tip: A prompt such as Starship works in Bash, Zsh, and Fish with the same config file. If you switch shells later, your prompt, colours, and git status line come with you.
Portability decides what you script in, and setup effort decides what you type in. The tables below split the comparison along those two axes.
Portability and compatibility:
| Factor | Bash | Zsh |
|---|---|---|
| POSIX-compliant | Yes | Mostly |
| On every server by default | Yes | No |
| Default on | Most Linux distros | macOS since 2019 |
Interactive experience:
| Factor | Zsh | Fish |
|---|---|---|
| Autosuggestions | Plugin | Built in |
| Config needed | Moderate | Almost none |
| POSIX scripting | Mostly | No, by design |
Other shells exist for narrower needs. Nushell treats pipelines as structured data and PowerShell does the same on Windows and Linux, but neither is a drop-in for Bash scripts, so the same rule applies: use them to type, not to ship automation.
Write every script with a #!/usr/bin/env bash shebang, whichever shell you use to type commands. This covers deploy pipelines, CI steps, cron jobs, and anything that runs on a server or in a container. This one habit removes the whole category of "works on my machine" failures caused by shell-specific syntax leaking into shared automation.
For your daily terminal, the choice comes down to how much setup you are willing to trade for control. Choose Fish if you want a great experience the moment you install it, with no dotfile archaeology. Choose Zsh if you have years of Bash muscle memory, existing dotfiles, or scripts you do not want to relearn syntax for. Keep Zsh startup under about 200 ms by profiling with zprof whenever a new tab feels slow.
If you are standardising tooling for a team at a company like Shopify or Atlassian, do not mandate an interactive shell. It has no bearing on production reliability. Mandate the Bash shebang convention for committed scripts instead, and treat Fish- or Zsh-specific syntax in a shared script as a code-review blocker.
š Remember: Using this approach does not mean avoiding Zsh or Fish at work. It means keeping them out of anything another machine has to execute.
Related reading: for the scripting side, start with writing Bash scripts and shell scripting for DevOps automation. For how your chosen shell loads its config, see configuring the Linux environment, PATH and dotfiles.
NoteReferences and Further Reading
- Fish Shell Documentation - Official tutorial and configuration reference
- Fish Release Notes - Version history including the 4.x series
- Oh My Zsh - Plugin and theme framework for Zsh
- GNU Bash Reference Manual - Official Bash scripting reference
- ShellCheck - Static analysis for shell scripts
No. Fish 4.0 was rewritten in Rust (early 2025) and current 4.x releases start in roughly the same range as a lean Zsh. The old "Fish is sluggish" reputation comes from earlier versions, so benchmark your own machine with hyperfine instead of trusting old posts.
The slowness is rarely Zsh itself. It is usually an Oh My Zsh install with many plugins, version managers like nvm loading eagerly, and a slow prompt. Add zmodload zsh/zprof to the top of .zshrc and run zprof to see which line costs the most.
CI runners usually ship only /bin/sh or Bash. Zsh arrays start at index 1, Zsh does not word-split unquoted variables, and Zsh-only features like setopt have no Bash equivalent. Put #!/usr/bin/env bash at the top of every committed script and test it with shellcheck.
macOS still ships Bash 3.2 as /bin/bash, and associative arrays need Bash 4 or newer. A shebang of #!/usr/bin/env bash picks the first bash in PATH, so install a current Bash with Homebrew, or rewrite the script without Bash 4 features.
Yes, for interactive use. Tools run scripts through their own shebangs or /bin/sh, so a Fish login shell rarely breaks them. Problems appear with sourced files, such as env.sh from a vendor, which Fish cannot read. Convert those to Fish syntax or use a wrapper like bass.
Discussion0