Emacs startup: Why setq beats setopt, customize-set-variable, and use-package :custom?

Every millisecond counts during Emacs initialization. A potential performance cost in modern Emacs configurations is defaulting to setopt, customize-set-variable, or the use-package :custom keyword when a direct assignment with setq is sufficient.

What is the difference between setq and other ways to assign values to variables?

setq (and setq-default)

The setq and setq-default functions directly assigns a value to a variable, making it the fastest way to set a variable. (FYI: This is one of the reasons why projects such as the minimal-emacs.d and Doom Emacs use setq and setq-default.)

(The setq function updates a variable's value within the current scope; if the target variable is buffer-local, the assignment only affects the current active buffer while the global state remains unchanged. Conversely, setq-default explicitly modifies the global default value of a variable. Although both functions produce the same result for purely global variables, setq-default must be used to apply system-wide changes to buffer-local variables like tab-width or indent-tabs-mode.)

setopt, customize-set-variable, and use-package :custom

The setopt, customize-set-variable, and use-package :custom functions can be slower than setq. Depending on the variable, they can:

  • Call the variable's defcustom :set function.
  • Validate the value against the variable's declared type.
  • In some cases, load the library defining an autoloaded customizable variable, potentially causing a package to load earlier during startup.

Note: The article assumes that the reader has a certain level of comfort with Emacs Lisp. For users who prefer a hands-off experience, defaulting to setopt or the built-in customization interface is often the better choice for convenience, even though they are not the fastest.

Example of an expansive defcustom :set property

To understand the latency penalty, look at how a package author might write a defcustom with an expensive :set property:

(defcustom my-global-visual-indicator t
  "Toggle a heavy visual indicator across all open buffers."
  :type 'boolean
  :group 'my-ui-package
  :set (lambda (symbol value)
         ;; Update the variable's value
         (set-default symbol value)
         
         ;; The slow part: Iterate through every open buffer
         ;; and trigger a costly visual update or cache rebuild.
         (dolist (buffer (buffer-list))
           (with-current-buffer buffer
             ;; This simulated function might parse the buffer,
             ;; apply text properties, or query a language server.
             (my-heavy-visual-update-function value)))
             
         ;; Force Emacs to immediately redraw all frames
         (redraw-display)))
Code language: Lisp (lisp)

Using setopt, customize-set-variable, or the use-package :custom to configure my-global-visual-indicator uses executes the associated defcustom :set function during startup:

;; Example 1: SLOW: This triggers the expensive :set function during startup
(setopt my-global-visual-indicator nil)

;; Example 2: This triggers the expensive :set function
(use-package my-ui-package
  :commands (my-ai-package-cmd1 my-ai-package-cmd2) ; defer
  :custom
  (my-global-visual-indicator nil))Code language: Lisp (lisp)

In many cases, the Emacs startup phase is simply not the appropriate time to execute those :set functions.

On the other hand, if the variable does not require its Custom setter to establish associated state, using setq inside a use-package :init block bypasses the Emacs Customization setter entirely:

;; Example 1: FAST: This bypasses the :set function for a faster startup
(setq my-global-visual-indicator nil)

;; Example 2: FAST: This bypasses the :set function
(use-package my-ui-package
  :commands (my-ai-package-cmd1 my-ai-package-cmd2) ; defer
  :init
  (setq my-global-visual-indicator nil))Code language: Lisp (lisp)

Note: Code placed in the :init block executes before the package loads into memory. Setting variables here ensures your customized values are already established in the global environment before the package evaluates its internal definitions, bypassing the need for a defcustom setter to update its internal state.

Trade-offs when using setq

Here are the trade-offs to consider when using setq:

  • During startup, a direct assignment can avoid UI updates or other side effects performed by a Custom setter. However, setq should only be used when the package does not require that setter to establish associated state.
  • setq does not validate types. If you pass a string where a boolean is expected, Emacs will not warn you at evaluation time.

Rule of thumb

Use setq by default.

Use setopt, customize-set-variable, or use-package :custom when a variable's Custom :set setter or type validation is required, or if you notice that the option is not being applied as intended.

(To determine whether a specific variable requires a custom :set setter for correct state initialization, inspect its properties directly within Emacs. Execute M-x describe-variable or C-h v and provide the variable's name to view its documentation. In the resulting help buffer, look for a :set attribute. While the presence of a :set function indicates that the package defines initialization logic for when the variable is modified, it does not necessarily mean that using setopt or use-package :custom is required. The necessity depends on the specific implementation, as the majority of major and minor modes automatically execute these initialization functions by default when the mode is activated.)