Optimizing Emacs Startup - Guide to Deferred Package Loading with use-package

As an Emacs user, your configuration can easily grow from a few lightweight adjustments to a massive, hundred-package IDE. Without careful management, Emacs startup time can degrade from sub-second execution to several seconds, or minutes, in the worst cases. Eager package loading is one common source of startup overhead. This guide explains how Emacs loads libraries, how use-package configures package loading, and how deferred loading can reduce startup time.

Before we dive in, please consider sharing this article on your website, blog, Mastodon, Reddit, X, Linkedin, or other social media platforms.

Why does Emacs startup get slow?

A naive package declaration looks like this:

;; The kirigami Emacs package provides a unified method to fold and unfold
;; text in Emacs across a diverse set of Emacs modes.
(use-package kirigami)

By default, this causes use-package to load the package during initialization, unless the declaration contains a deferring keyword or another option that changes the loading behavior.

Conceptually, the eager loading is equivalent to:

(require 'kirigami)

The require function ensures that a feature is loaded.

When many packages are loaded this way, the time spent loading and evaluating their code contributes to increasing startup time.

What is Autoloading?

Autoloading registers a function without immediately loading the library that defines it. The library is loaded only when the function is called. For example:

;; Simplified representation of an autoload registration
(autoload 'kirigami-global-mode "kirigami" "Global mode for Kirigami." t)

If M-x kirigami-global-mode invokes the autoload, Emacs loads the kirigami.el library ("kirigami", the second argument), and then calls the actual kirigami-global-mode function. This allows the library's startup cost to be deferred until the function is actually needed.

Explicit vs. implicit deferral in use-package

The use-package macro makes package configuration and loading declarative. By default, a declaration is eager, meaning the package is loaded immediately while Emacs processes the configuration during startup, rather than waiting until the package is needed. A deferring keyword or another loading mechanism can postpone loading until later.

The explicit :defer keyword

Deferral using :defer t (Not necessary)

Note: Explicitly using :defer t is often unnecessary. Keywords such as :commands, :bind, :hook, :commands, and :mode establish loading triggers automatically. These keywords are discussed below.

If a package does not have another loading trigger, :defer t prevents use-package from loading it immediately:

(use-package kirigami
  :defer t)

This prevents the package from being required during startup. Without an autoload or another loading trigger, the package will remain unloaded until something else loads it.

Idle deferral: Numeric argument (Not ideal)

A numeric value to :defer schedules the package to be loaded after Emacs has been idle for the specified number of seconds:

;; Load kirigami after Emacs has been idle for 20 seconds
(use-package kirigami
  :defer 20)

Note: This is not ideal because loading a heavy package after a few seconds will block the event loop and can cause a freeze in the user interface. If multiple packages are deferred via idle timers, they can trigger sequentially during a single idle period, increasing the delay. To prevent unprompted blocking operations, it is best practice to configure explicit autoloads, such as :hook, :bind, :commands, or :mode. (These keywords are discussed below.)

Implicit deferral: Autoload triggers (Recommended solution)

Instead of manually adding :defer t to every use-package declaration, you can use trigger keywords to configure deferred loading automatically. These keywords generate autoloads, ensuring the package evaluates only when you interact with its components:

  • :commands: Generates autoloads for specific interactive commands. Emacs loads the package when you execute the command (for example, via M-x).
  • :bind: Maps keys to commands and automatically creates autoloads for them.
  • :hook: Adds a package function to a hook and arranges for deferred loading.
  • :mode: Adds a file-pattern entry to auto-mode-alist and defers loading until the mode is needed.

Example: Autoloading on major mode file extensions

Instead of loading Markdown mode immediately, use :mode to defer it until you open a .md file:

(use-package markdown-mode
  :mode ("\\.md\\'" . markdown-mode))

Note: Specifying :defer t is unnecessary here, as :mode automatically defers package loading.

Example: Autoloading on keybindings

The :bind keyword maps keys to commands and automatically creates autoloads for them. The package remains deferred until you press one of the defined key combinations:

(use-package embark
  :bind
  (("C-." . embark-act)       
   ("C-;" . embark-dwim)       
   ("C-h B" . embark-bindings)))

Example: Autoloading specific commands

If you do not want to define global keybindings but still need to defer loading until a command is called interactively (for example, via M-x), use the :commands keyword. This generates the necessary autoloads:

(use-package embark
  :commands (embark-act
             embark-dwim
             embark-bindings))

Example: Autoloading on hook execution

Instead of eagerly loading outline-indent-minor-mode (indentation-based code folding) at startup, you can configure Emacs to load and activate it on demand. This ensures the package evaluates only when a python-mode, python-ts-mode, or yaml-ts-mode buffer is opened:

;; The outline-indent Emacs package provides a minor mode for
;; indentation-based code folding.
(use-package outline-indent
  :commands (outline-indent-minor-mode
             outline-indent-backward-same-level
             outline-indent-forward-same-level)
  :hook ((python-mode . outline-indent-minor-mode)
         (python-ts-mode . outline-indent-minor-mode)
         (yaml-ts-mode . outline-indent-minor-mode))
  :custom
  (outline-indent-ellipsis " ▼"))

This also loads outline-indent when you invoke the command manually via M-x outline-indent-minor-mode, outline-indent-backward-same-level, or outline-indent-forward-same-level.

Note: There is no need to specify :defer t separately. The :hook keyword adds outline-indent-minor-mode to python-mode-hook, python-ts-mode-hook, and yaml-ts-mode-hook.

The unconditional use-package :init block

Code inside the use-package :init section is evaluated before the package is loaded. An autoloaded function called from :init can itself cause the package to load immediately, defeating the intended deferral. Therefore, :init is generally appropriate for settings that must be established before the package loads, while package-dependent function calls belong in :config or another deferred trigger (e.g., with-eval-after-load).

(Similarly, code inside the :preface block evaluates before package loading, but it also executes during byte-compilation. This distinction makes :preface the correct location for code required to satisfy the byte-compiler, such as defvar statements, helper macro definitions, or declare-function calls that prevent compiler warnings for deferred libraries.)

Forcing immediate load with :demand

With global deferral enabled, you will occasionally encounter packages that must be loaded immediately (e.g., themes, keybinding managers, or daemon-side servers). You can override global deferral on an individual basis using :demand t:

(use-package tomorrow-night-deepblue-theme
  :demand t ; Force immediate loading
  :config
  ;; Load the tomorrow-night-deepblue theme
  (load-theme 'tomorrow-night-deepblue t))

Note: If a declaration specifies both :demand t and :defer t (or triggers), :demand t takes precedence and forces eager loading.

The use-package :after keyword

The use-package :after keyword allows you to defer the initialization of a package until one or more specified target packages have been fully loaded. This is practical for:

  • Add-on packages that extend a base minor mode.
  • Integration packages that bridge two distinct tools together.

Consider embark-consult, a package that integrates embark with consult. You only need the embark-consult package after embark and consult are loaded:

(use-package embark-consult
  :after (embark consult))

In this scenario, embark-consult remains entirely inactive during startup. Emacs won't evaluate the embark-consult configuration until it has successfully loaded both embark and consult.

Forcing immediate loading

Consider this configuration:

(use-package kirigami
  :defer t
  :custom
  (kirigami-show-menu-bar t)
  (kirigami-show-context-menu t)
  :init
  (kirigami-global-mode 1)) ; This forces immediate loading

Although :defer t is present, evaluating (kirigami-global-mode 1) during :init triggers the autoload for kirigami-global-mode. The package is therefore loaded during initialization, defeating the intended deferral.

Loading after the Emacs init phase

If you prefer to load a package after the init phase, use the :hook keyword:

(use-package kirigami
  :custom
  (kirigami-show-menu-bar t)
  (kirigami-show-context-menu t)
  :hook (after-init . kirigami-global-mode)) ; Activate after init

Using :hook (after-init . kirigami-global-mode) adds the mode to after-init-hook. This ensures the package remains deferred while Emacs evaluates the rest of your init file, guaranteeing the mode is active before the editor is ready for user interaction.

Global laziness: use-package-always-defer (Not recommended)

You can configure use-package to automatically assume :defer t for declarations that do not otherwise establish eager loading:

(setq use-package-always-defer t) ; NOT RECOMMENDED

This changes the default behavior so that use-package declarations are deferred unless a declaration explicitly requires immediate loading or establishes another loading mechanism.

Note: Global deferral breaks packages that rely on background execution, global hooks, or immediate side effects to function correctly. You will find yourself tracking down silent failures and adding :demand t throughout your initialization file to force eager loading for things like themes, modeline managers, and daemon servers. Instead of relying on a blanket setting that obscures the loading state, setting use-package-always-defer to nil and explicitly deferring packages where appropriate results in a more deterministic setup.

Diagnostics and verification

To check whether a package feature has been provided in the current Emacs session, use the featurep function. For example, for the kirigami package:

(featurep 'kirigami)
  • Evaluates to nil when the feature has not been provided.
  • Evaluates to t when the feature has been provided.

Debugging and macro expansion optimization

If you need to diagnose loading order or macro expansion issues, use-package provides built-in settings to help debugging and performance.

Setting use-package-expand-minimally to t causes the use-package macro to generate less boilerplate code. If you byte-compile your initialization file, this reduces the size of the compiled file and can yield slight performance improvements:

(setq use-package-expand-minimally t)

Setting use-package-verbose to t forces use-package to log package loading events to the *Messages* buffer. This is useful for verifying whether deferred packages are loading when expected:

(setq use-package-verbose t)

Setting use-package-enable-imenu-support to t causes imenu to see use-package declarations. This allows you to quickly jump to specific use-package blocks using M-x imenu:

(setq use-package-enable-imenu-support t)

By default, the :hook keyword automatically appends -hook if you omit it. Setting use-package-hook-name-suffix to nil disables this behavior. Disabling this forces you to write fully qualified hook names (e.g., (python-mode-hook . outline-indent-minor-mode)):

(setq use-package-hook-name-suffix nil) ; NOT RECOMMENDED

Note: It is not recommended to set use-package-hook-name-suffix to nil if you maintain a pre-existing configuration that relies on the default implicit behavior. Modifying this variable globally instantly breaks every existing :hook (mode . function) declaration that omits the -hook suffix. Unless you are building a configuration from scratch or can rewrite and validate every hook assignment in a single pass, leaving the default behavior intact is the most practical decision.

Startup profiling: measuring the impact

To benchmark startup optimizations, measure startup at a point later than the end of init-file processing.

Benchmarking startup

For an accurate startup-time measurement, read:
Measuring Emacs startup time more accurately than the built-in emacs-init-time

Profiling individual packages

use-package can collect loading statistics. Enable statistics gathering after use-package has been loaded but before the use-package declarations are evaluated:

(setq use-package-compute-statistics t)

After restarting Emacs, execute: M-x use-package-report

This displays a tabulated buffer with timing information for the use-package phases.

Advanced profiling with benchmark-init

While use-package-report offers insights into declarative package loading, the third-party package benchmark-init (available on MELPA) provides a more granular view of the entire Emacs startup process. It tracks the time spent loading individual files and executing functions across the whole initialization sequence, allowing you to see exactly where you can influence startup time.

Related links

Leave a Reply

Your email address will not be published. Required fields are marked *