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, viaM-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 toauto-mode-alistand 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
nilwhen the feature has not been provided. - Evaluates to
twhen 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.