Skip to content

Instantly share code, notes, and snippets.

@ctechols
Last active March 6, 2024 08:37
Star You must be signed in to star a gist
Save ctechols/ca1035271ad134841284 to your computer and use it in GitHub Desktop.
Speed up zsh compinit by only checking cache once a day.
# On slow systems, checking the cached .zcompdump file to see if it must be
# regenerated adds a noticable delay to zsh startup. This little hack restricts
# it to once a day. It should be pasted into your own completion file.
#
# The globbing is a little complicated here:
# - '#q' is an explicit glob qualifier that makes globbing work within zsh's [[ ]] construct.
# - 'N' makes the glob pattern evaluate to nothing when it doesn't match (rather than throw a globbing error)
# - '.' matches "regular files"
# - 'mh+24' matches files (or directories or whatever) that are older than 24 hours.
autoload -Uz compinit
if [[ -n ${ZDOTDIR}/.zcompdump(#qN.mh+24) ]]; then
compinit;
else
compinit -C;
fi;
@sparcbr
Copy link

sparcbr commented Aug 3, 2020

A few notes about the this gist..
The file age test code seems to work even without #q or setopt extendedglob. Not quite sure why, but would need to check the zsh man page.

# 3 hours and older .zcompdump:
❯ [[ -n ${ZDOTDIR:-$HOME}/.zcompdump(N.mh+2) ]] &&  echo yes       
yes

Skipping Ubuntu system-wide compinit

Don't forget to check if the system wide zshrc is running compinit, and disable it if you can. So you can run your own compinit startup code.
Ubuntu does this in /etc/zsh/zshrc, and you can disable by adding this to your ~/.zshenv:

# Skip the not really helping Ubuntu global compinit
skip_global_compinit=1

Using Zinit - a fast Zsh plugin manager

Zinit provides a way to intercept compdef calls (used completion by completion scripts) to execute them later, after compinit, using the zinit cdreplay command:

# ~/.zshrc
source ~/.zinit/bin/zinit.zsh
zinit load "some/plugin"
zinit load "other/plugin"
autoload -Uz compinit; compinit
zinit cdreplay

More info:
https://github.com/zdharma/zinit#calling-compinit-without-turbo-mode

@faaizajaz
Copy link

I am totally confused. Where is everyone adding these snippets? I have tried adding to my .zshrc to no success. The original gist mentions "zsh completion file" but what is that? Google turns up nothing.

@duraki
Copy link

duraki commented Oct 9, 2020

I agree w/ @faaizajaz question; where do we put this stuff?

@ctechols
Copy link
Author

ctechols commented Oct 9, 2020

@faaizajaz, @duraki I can't speak with specifics about most of the additional discussion on this gist. (I no longer use any of these approaches) In general though, you are going to want to make your changes right after you load the compinit module. This is likely to be the file that compinstall created or edited.

To find this file on your system try zstyle | grep compinstall

@duraki
Copy link

duraki commented Oct 11, 2020

Thanks @ctechols

This is how I did it.

I created this folder:

  • mkdir ~/.oh-my-zsh/plugins/zshfl

Inside I've put:

  • touch zshfl.plugin.zsh
# compinit optimization for oh-my-zsh
# On slow systems, checking the cached .zcompdump file to see if it must be
# regenerated adds a noticable delay to zsh startup.  This little hack restricts
# it to once a day.  It should be pasted into your own completion file.
#
# The globbing is a little complicated here:
# - '#q' is an explicit glob qualifier that makes globbing work within zsh's [[ ]] construct.
# - 'N' makes the glob pattern evaluate to nothing when it doesn't match (rather than throw a globbing error)
# - '.' matches "regular files"
# - 'mh+24' matches files (or directories or whatever) that are older than 24 hours.
#autoload -U compaudit compinit
#: ${ZSH_DISABLE_COMPFIX:=true}
# ...
setopt extendedglob
if [[ $ZSH_DISABLE_COMPFIX != true ]]; then
# If completion insecurities exist, warn the user without enabling completions.
if ! compaudit &>/dev/null; then
# This function resides in the "lib/compfix.zsh" script sourced above.
    handle_completion_insecurities
# Else, enable and cache completions to the desired file.
else
if [[ -n "${ZSH_COMPDUMP}"(#qN.mh+24) ]]; then
      compinit -d "${ZSH_COMPDUMP}"
      compdump
else
      compinit -C
fi
fi
else
if [[ -n "${ZSH_COMPDUMP}"(#qN.mh+24) ]]; then
    compinit -i -d "${ZSH_COMPDUMP}"
    compdump
else
    compinit -C
fi
fi

And I've loaded it through .zshrc in plugins=(zshfs git ...)

@ftlno
Copy link

ftlno commented Oct 27, 2020

Thanks @ctechols

This is how I did it.

I created this folder:

  • mkdir ~/.oh-my-zsh/plugins/zshfl

Inside I've put:

  • touch zshfl.plugin.zsh
# compinit optimization for oh-my-zsh
# On slow systems, checking the cached .zcompdump file to see if it must be
# regenerated adds a noticable delay to zsh startup.  This little hack restricts
# it to once a day.  It should be pasted into your own completion file.
#
# The globbing is a little complicated here:
# - '#q' is an explicit glob qualifier that makes globbing work within zsh's [[ ]] construct.
# - 'N' makes the glob pattern evaluate to nothing when it doesn't match (rather than throw a globbing error)
# - '.' matches "regular files"
# - 'mh+24' matches files (or directories or whatever) that are older than 24 hours.
#autoload -U compaudit compinit
#: ${ZSH_DISABLE_COMPFIX:=true}
# ...
setopt extendedglob
if [[ $ZSH_DISABLE_COMPFIX != true ]]; then
# If completion insecurities exist, warn the user without enabling completions.
if ! compaudit &>/dev/null; then
# This function resides in the "lib/compfix.zsh" script sourced above.
    handle_completion_insecurities
# Else, enable and cache completions to the desired file.
else
if [[ -n "${ZSH_COMPDUMP}"(#qN.mh+24) ]]; then
      compinit -d "${ZSH_COMPDUMP}"
      compdump
else
      compinit -C
fi
fi
else
if [[ -n "${ZSH_COMPDUMP}"(#qN.mh+24) ]]; then
    compinit -i -d "${ZSH_COMPDUMP}"
    compdump
else
    compinit -C
fi
fi

And I've loaded it through .zshrc in plugins=(zshfs git ...)

Thank you!

Did you mean plugins=(zshfl git ... ? :)

@divyekapoor
Copy link

Just check if the shell is an interactive shell before executing compinit?

eg. with [[ -o interactive ]] ?

@MostHated
Copy link

MostHated commented Mar 14, 2021

Skip the not really helping Ubuntu global compinit

skip_global_compinit=1

Just adding this one thing to .zshenv cut my initial loading time in half. 👍

For plugins, though. I use this: https://getantibody.github.io/ "The fastest shell plugin manager."

Plus, here are some additional tips to make initial loading faster if anyone is interested.
https://carlosbecker.com/posts/speeding-up-zsh/

(Man, I didn't realize it's already been 5 years since I came across that site. So I suppose its a bit of an old post, but most of it should still be relevant and helpful)

@smac89
Copy link

smac89 commented Nov 8, 2021

Over time, I've improved this snippet to become something like this:

function {
    # http://zsh.sourceforge.net/Doc/Release/Options.html#Scripts-and-Functions
    setopt LOCAL_OPTIONS extendedglob nullglob
    # http://zsh.sourceforge.net/Doc/Release/Expansion.html#Glob-Qualifiers
    # http://zsh.sourceforge.net/Doc/Release/Conditional-Expressions.html#Conditional-Expressions
    if [[ ! -e "${ZSH_COMPDUMP}" || -n ${~"${ZSH_COMPDUMP}"}(#qmh+24) ]]; then
        zi compinit > /dev/null
        zi cdreplay -q
    fi
}

A few notes:

  • zi comes from zinit
  • ZSH_COMPDUMP comes from ohmyzsh, which I use in addition to zinit
  • The ${~...} syntax is to enable glob expansion with braces because the name of the file is in a variable
  • Everything is in an anonymous function so that we can enable extendedglob just for the function, and not for the entire shell

@aztack
Copy link

aztack commented Dec 14, 2021

autoload -Uz compinit
for dump in ~/.zcompdump(N.mh+24); do
  compinit
done
compinit -C

works for me.

Timing with timezsh:

timezsh() {
  shell=${1-$SHELL}
  for i in $(seq 1 4); do /usr/bin/time $shell -i -c exit; done
}

Before:

        1.33 real         0.80 user         0.52 sys
        1.38 real         0.80 user         0.57 sys
        1.55 real         0.82 user         0.64 sys
        1.50 real         0.81 user         0.63 sys

After:

        0.78 real         0.32 user         0.42 sys
        0.81 real         0.33 user         0.45 sys
        0.96 real         0.37 user         0.55 sys
        1.21 real         0.42 user         0.71 sys

@xinxilas
Copy link

xinxilas commented Mar 1, 2022

It was faster here

Definitive solution?

@vincentbernat
Copy link

vincentbernat commented Apr 7, 2022

I am going for

() {
    emulate -L zsh
    setopt extendedglob
    autoload -Uz compinit complist
    local zcd=$1                # compdump
    local zcdc=$1.zwc           # compiled compdump
    local zcda=$1.last          # last compilation
    local zcdl=$1.lock          # lock file
    local attempts=30
    while (( attempts-- > 0 )) && ! ln $zcd $zcdl 2> /dev/null; do sleep 0.1; done
    {
        if [[ ! -e $zcda || -n $zcda(#qN.mh+24) ]]; then
            compinit -i -d $zcd
            : > $zcda
        else
            compinit -C -d $zcd
        fi
        [[ ! -f $zcdc || $zcd -nt $zcdc ]] && rm -f $zcdc && zcompile $zcd &!
    } always {
        rm -f $zcdl
    }
} $ZSH/run/u/$HOST-$UID/zcompdump

I didn't see a mention what happens when multiple shells are spawned at the same time. Maybe that's the reason we sometime run into empty dump file.

@medwatt
Copy link

medwatt commented Apr 12, 2022

Where exactly do we put this code:

autoload -Uz compinit
for dump in ~/.zcompdump(N.mh+24); do
  compinit
done
compinit -C

@faelin
Copy link

faelin commented Jun 3, 2022

@medwatt put the snippet somewhere in your .zshrc file, or put it in an external script file and call it from your .zshrc

@thefotios
Copy link

@vincentbernat I solved the multiple shell problem by using a lockfile and a trap to remove it. This ensures that only one process is responsible for updating the file. Even though there's still a race condition, it's now almost impossibly small 1

() {
	setopt local_options
	setopt extendedglob

	local zcd=${1}
	local zcomp_hours=${2:-24} # how often to regenerate the file
	local lock_timeout=${2:-1} # change this if compinit normally takes longer to run
	local lockfile=${zcd}.lock

	if [ -f ${lockfile} ]; then 
		if [[ -f ${lockfile}(#qN.mm+${lock_timeout}) ]]; then
			(
				echo "${lockfile} has been held by $(< ${lockfile}) for longer than ${lock_timeout} minute(s)."
				echo "This may indicate a problem with compinit"
			) >&2 
		fi
		# Exit if there's a lockfile; another process is handling things
		return
	else
		# Create the lockfile with this shell's PID for debugging
		echo $$ > ${lockfile}
		# Ensure the lockfile is removed
		trap "rm -f ${lockfile}" EXIT
	fi

	autoload -Uz compinit

	if [[ -n ${zcd}(#qN.mh+${zcomp_hours}) ]]; then
		# The file is old and needs to be regenerated
		compinit
	else
		# The file is either new or does not exist. Either way, -C will handle it correctly
		compinit -C
	fi
} ${ZDOTDIR:-$HOME}/.zcompdump 

For those who are new to some of this stuff, check out

Footnotes

  1. A second process would have to check for the file in between 2 (effectively) subsequent commands. if [ -f ${lockfile} ] and echo $$ > ${lockfile}

@vincentbernat
Copy link

@thefotios my solution is already handling this case (but my message wasn't quite clear on that, so I understand you thought this was not the case). This is the purpose of the ln command (which is atomic, so no race condition).

@rafpaf
Copy link

rafpaf commented Dec 24, 2022

I commented out these lines in my .zshrc which sped it up a lot:

if type brew &>/dev/null; then
    FPATH=$(brew --prefix)/share/zsh-completions:$FPATH

    autoload -Uz compinit
    compinit
if

As this comment above pointed out, oh-my-zsh already runs compinit.

@cattokomo
Copy link

@aztack Thank you for your snippet, it helps alot :)

@acid-bong
Copy link

Fellas, if you're of that kind that checks their .zsh files and scripts with shellcheck (like I am), here's a more POSIX-compliant (as much as zsh allows) statement:

if [ "$(find ~/.zcompdump -mtime 1)" ] ; then
    compinit
fi
compinit -C

or oneliner, if you prefer that:

# negation, so that at least one exits on 0
[ ! "$(find ~/.zcompdump -mtime 1)" ] || compinit
compinit -C

@eeweegh
Copy link

eeweegh commented Sep 16, 2023

find's manpage is not clear on this, but I believe you want -mtime +1 to catch a file at least 24h old, rather than exactly 24h old.
On OSX, my .zcomdump was several days old, and the above would not trigger until I added the '+'.

@nimitagr
Copy link

nimitagr commented Oct 1, 2023

@aztack It helped. 🙇

@niqodea
Copy link

niqodea commented Jan 21, 2024

This is my take on the problem, it's a tradeoff between efficiency and simplicity:

autoload -Uz compinit; compinit -C  # Use cache to reduce startup time by ~0.1s
# Have another thread refresh the cache in the background (subshell to hide output)
(autoload -Uz compinit; compinit &)

Despite the obvious pitfall (having the shell start another thread at startup), I wonder if it's overall a good solution 🤔

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment