Lazy Loading Emacs Init with use-package: Defer Heavy Packages Until First Use
Use use-package to declare lazy loading for heavy Emacs packages. Defer load until first command or mode activation, keep init declarative, and verify with emacs-init-time and macro expansion.
16 Sept 2025, 09:21 UTC

Emacs startup feels slow because init files eagerly require packages. The useful takeaway is to keep the init declarative and let packages load only on first use. use-package gives you a macro-based DSL for that decision without scattering require calls through init.el.
The concrete problem: init time dominated by eager loads
Emacs loads your init file sequentially. A plain (require 'magit) or (require 'lsp-mode) runs the entire package at startup, allocating memory and evaluating code you may not need for minutes. On Emacs 27+ with native compilation, the cost is still paid up front for parsing and loading.
use-package is not part of Emacs core, but it is widely adopted. It lets you declare when a package should be available: on a command, on a mode hook, or never until explicitly required. The macro expands at byte-compile time into autoloads and with-eval-after-load forms, so the load behavior is inspectable.
How deferral works in practice
The keywords that matter are :defer, :commands, :hook, :bind and :config.
- :defer t or :defer 0.5 postpones loading until first use. With :defer t the package is not loaded during init.
- :commands declares autoload stubs for specific interactive commands. The first invocation triggers the real load.
- :hook attaches the package to a mode hook via with-eval-after-load, so the package loads only when that mode is enabled.
- :config runs after the package is loaded, not at init time.
This separation keeps the init file readable and moves the actual require to the moment of need.
Worked example: deferring a heavy package
Place this in your init file after you ensure use-package is available. No special permissions are needed; evaluate in *scratch* or load via init.el.
(use-package magit
:defer t
:commands (magit-status magit-log-current)
:bind ("C-x g" . magit-status)
:config
(setq magit-display-buffer-function #'magit-display-buffer-same-window-except-diff-v1))
What this declares:
- Do not load magit at startup.
- Create autoloads for magit-status and magit-log-current so the keybinding works.
- When you press C-x g or call magit-status, Emacs loads magit then runs the :config body once.
A similar pattern for a mode-heavy package:
(use-package yaml-mode
:mode "\\.ya?ml\\'"
:hook (yaml-mode . yaml-mode-setup))
Here yaml-mode is autoloaded on file open, not at init.
Trade-off: later errors and hidden load order
Deferral improves perceived startup but delays error detection. A misspelled option in :config will not surface until the package loads on first use, making the first command slower and the failure later.
Load order becomes subtle when multiple use-package forms depend on each other. Because the macro hides expansion, you should verify rather than assume. Macro expansion is visible with M-x pp-macroexpand-last-sexp after evaluating a use-package form. Look for with-eval-after-load and autoload definitions.
use-package behavior is stable across Emacs 27+, but native-comp and tree-sitter integrations are version-sensitive. :defer semantics can differ slightly between Emacs 27, 28 and 29+. Treat use-package as third-party: core Emacs guarantees apply only to built-in autoload mechanisms.
Verify the decision yourself
- Check init duration: start Emacs with emacs --debug-init and after startup run M-x emacs-init-time. Compare a session with and without :defer for the same package.
- Confirm lazy load: before first use, M-x describe-function RET magit-status shows an autoload stub. After first invocation, describe-function shows the real definition.
- Inspect expansion: evaluate a use-package form in *scratch*, then M-x pp-macroexpand-last-sexp to see generated autoloads and with-eval-after-load.
Risk: deferring too aggressively can make the first command feel slow. Keep critical interactive commands eagerly available if they are part of your core workflow.
Actionable closing: audit your init for top-level requires, wrap heavy packages in use-package with :defer t or :hook, keep :config for post-load settings, and verify load state with describe-function. That gives a declarative, readable init with measurable startup improvement and predictable first-use behavior.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.