Skip to content
Shell & TerminalDeep Dive Published Updated 7 min readViews unavailable

Zsh zcompile: Build and Invalidate Word-Code Caches Safely

Use zcompile for Zsh scripts and autoloaded functions with correct digest handling, freshness checks, version validation, and reproducible rebuilds.

Zsh’s zcompile builtin compiles shell scripts and function definitions into word-code files. When Zsh reads an eligible compiled file, it can avoid reparsing the text source, which may reduce the cost of sourcing a file or autoloading a function. It is an optimization for code that is already correct and measurably expensive to parse, not a substitute for reducing unnecessary startup work.

The relevant artifact is .zwc, short for Zsh word code. A per-file compiled artifact sits beside its source, while a digest file can contain several functions and be used as an element of fpath. Zsh’s search and freshness rules determine whether compiled code is used. Build scripts therefore need to treat word-code files as derived artifacts with explicit inputs, outputs, and invalidation behavior.

Compile a single source file

With one input file and no explicit output path, zcompile creates a file named after the source plus .zwc in the same directory. For example:

zcompile "$HOME/.zshrc"

The shell can use the compiled form when it is newer than and corresponds to the source file. Editing the source makes the compiled artifact stale, so the next startup should rebuild it or fall back to interpreting the source according to the documented lookup conditions. Do not commit a stale .zwc and assume it represents the current .zshrc.

Compiling a large startup file may save some parsing time, but it does not eliminate the work performed by plugins, external commands, version managers, or prompt hooks. Profile startup first, then compile only files where parsing is a meaningful share of the measured cost. A cache adds a build step and a freshness concern; if the saved time is negligible, remove the complexity.

Build a digest for autoloaded functions

When several function source files are passed after the output name, zcompile can create a digest file containing their compiled definitions. A digest is intended to be discoverable through fpath and can make a set of autoloaded functions available from a single compiled artifact.

zcompile "$HOME/.zsh/functions.zwc" \
    "$HOME/.zsh/functions/_project_status" \
    "$HOME/.zsh/functions/_project_clean"

The first argument is the output file; subsequent arguments are source files to compile. If the output name does not end in .zwc, Zsh appends that extension. Put the digest in a directory included in fpath when the intended consumer is autoload resolution. Do not confuse a per-source compiled sibling with a digest containing multiple named functions; their search and rebuild contracts differ.

The compiler also has options for compiling functions already defined in the current shell (-c) and functions marked for autoload (-a). These modes are not interchangeable. A source file may define several functions and execute initialization code after the definitions; compiling only one loaded function can omit the other definitions or the trailing code. Use the autoload-aware form when that complete source-file behavior is required, and verify the generated artifact contains the names you intend.

Validate what was compiled

zcompile -t examines a compiled file. Without function names, it reports the original files included and information about the shell version that compiled the file and how it will be used. With names, it can check whether the requested definitions are present and return a success or failure status.

zcompile -t "$HOME/.zsh/functions.zwc"
zcompile -t "$HOME/.zsh/functions.zwc" _project_status _project_clean

Use this check in a build or dotfiles test instead of merely checking that the file exists. An artifact can exist but contain an older or incomplete set of functions. A name check also makes a digest’s intended public surface testable as the collection evolves.

Compile from a clean, controlled shell with the same Zsh release and relevant options expected at runtime. Zsh documents version information in the compiled file’s inspection output; treat that as compatibility evidence and rebuild artifacts when changing the supported shell runtime. Avoid copying a developer’s prebuilt .zwc into a different architecture or Zsh version without checking the documented format and testing the target environment.

Make freshness a build-system rule

For a per-file .zwc, Zsh’s lookup rule requires the compiled file to be newer than the text source and to be the compiled form of that source. Make this a build dependency: whenever the source changes, rebuild the associated word-code file. For a digest, track every source file as an input and regenerate the digest whenever any member changes. File timestamps can be unreliable across archive extraction, network filesystems, or reproducible builds, so deployments should either preserve valid timestamps or rebuild artifacts during installation.

A small explicit build function is easier to audit than compiling opportunistically from .zshrc on every shell launch:

compile_zsh_functions() {
    local output=$HOME/.zsh/functions.zwc
    local source

    zcompile "$output" \
        "$HOME/.zsh/functions/_project_status" \
        "$HOME/.zsh/functions/_project_clean" || return

    zcompile -t "$output" _project_status _project_clean
}

The function should return a failure status if either compilation or validation fails. It writes to a predictable output path, which helps a dotfiles test inspect the artifact. For a larger project, use a temporary output and replace the active digest only after validation succeeds; this avoids exposing a partial artifact if a build is interrupted. Ensure the directory is owned and writable only by the intended user.

Do not make ordinary interactive startup compile files without a clear reason. It can add latency, hide a broken source tree behind stale binaries, or fail when the home directory is read-only. Build during dotfiles installation or a dedicated update step, and leave a readable source fallback available.

Avoid confusing compilation with semantic validation

Word-code generation does not prove that a function uses safe quoting, handles errors correctly, or preserves the caller’s options. It does not replace shell syntax checks or tests. Run zsh -n against the source, then test the compiled path in an isolated shell and compare behavior with the plain source. Include autoload resolution and function-name checks if a digest is involved.

If function files rely on option state, emulation mode, fpath order, or environment values, test those runtime inputs explicitly. The same compiled function can behave differently under a different option set because zcompile stores parsed code, not a complete shell session snapshot. Initialize only the required functions and options in the test so the artifact’s dependencies are visible.

A safe rollout can keep a text-only path available until the compiled artifact has been tested. Build the .zwc in a staging directory, inspect its contents, start a clean Zsh process with the intended fpath, and call a representative function. If it fails, remove the artifact and run the source form; never delete the original function directory as a performance shortcut.

Diagnose stale or ignored word code

Check the source and compiled file timestamps, the output file’s name, and the actual fpath order. A .zwc in a directory not searched by the shell cannot accelerate autoloading from another location. A digest must be placed and named so that the function autoload mechanism discovers it. Use zcompile -t to inspect the artifact rather than inferring its contents from the build command’s success.

If a changed function appears to run old code, verify that the source timestamp is newer than the sibling .zwc or explicitly rebuild the digest. Also check whether the function was already loaded into the current shell: replacing the file does not necessarily replace a function definition already resident in memory. Start a fresh process to test the updated artifact. This distinction avoids “cache invalidation” fixes that actually require a new shell session.

If word-code inspection reports the wrong shell version or missing function names, rebuild with the target Zsh and the exact intended input files. Do not suppress a build error and leave an old artifact in place. A deployment script should fail clearly or remove the stale output so that the source form is used; silently continuing with a known-old word-code file makes the deployed code ambiguous.

Measure the optimization

Record startup time before and after using the same terminal, machine, environment, and shell options. Separate startup parsing from slow external commands or plugin initialization. Compare cold and warm launches if the filesystem cache matters, and repeat enough trials to avoid treating a single noisy result as a win. Keep the source, build command, and test environment under version control, but decide deliberately whether compiled outputs themselves should be committed or generated during installation.

zcompile is a useful optimization when its performance benefit is measured and its artifact is rebuilt deterministically. Keep source as the reviewable authority, define cache inputs explicitly, check the compiled file with -t, and test it in a fresh shell. That approach preserves the convenience of faster parsing without turning a hidden binary cache into a second, stale version of the dotfiles.

Related:

Sources:

Comments