Using use-package the right way
I recently wrote that Emacs startup time doesn’t matter and I got quite a lot of heat for it. I totally stand by everything I said there, but I acknowledge that different people have different use-cases and perspectives when it comes to this.
That’s why I’ve decided to share with you the #1 tip to speed up your
Emacs - defer the load time of your packages (in other words - load them as
late as possible, ideally when you actually need them for the first time). There
are many ways to achieve this, but probably the easiest and most popular these
days is to use use-package
to organize your package configuration.
Unfortunately, using use-package
the right way is not very obvious and there’s plenty
of incorrect information about it all over the Internet. Here’s classic example of
a problematic use-package
usage:1
(use-package projectile
:init
(setq projectile-project-search-path '("~/projects/" "~/work/" "~/playground"))
:config
;; I typically use this keymap prefix on macOS
(define-key projectile-mode-map (kbd "s-p") 'projectile-command-map)
;; On Linux, however, I usually go with another one
(define-key projectile-mode-map (kbd "C-c C-p") 'projectile-command-map)
(global-set-key (kbd "C-c p") 'projectile-command-map)
(projectile-mode +1))
While this is technically speaking correct, the use of :init
and :config
means that the
package will be loaded immediately.
You might be wondering at this point when to use things like :preface
, :config
and
:init
and you would be right to. As usual, the best answer is in the Emacs
manual,
and I’ll try to expand on it below.
Where possible, it is better to avoid :preface
, :config
and :init
.
Instead, prefer autoloading keywords such as :bind
, :hook
, and :mode
, as
they will take care of setting up autoloads for you without any need for
boilerplate code. While the usage of preface
in the wild is fairly rare, you’ll
see a ton of usage of :init
and :config
for whatever reasons.2
For example, consider the following declaration:
(use-package foo
:init
(add-hook 'some-hook 'foo-mode))
This has two problems. First, it will unconditionally load the package
foo
on startup, which will make things slower. You can fix this by
adding :defer t
:
(use-package foo
:defer t
:init
(add-hook 'some-hook 'foo-mode))
This is better, as foo
is now only loaded when it is actually needed
(that is, when the hook some-hook
is run).
The second problem is that there is a lot of boilerplate that you
have to write. In this case, it might not be so bad, but avoiding that
was what use-package
was made to allow. The better option in this case
is therefore to use :hook
, which also implies
:defer t
. The above is thereby reduced down to:
(use-package foo
:hook some-hook)
Now use-package
will set up autoloading for you, and your Emacs
startup time will not suffer one bit. Nice, ah?
So, let’s return now to our original example and think how we can improve it. Our first instinct is probably to do something like:
(use-package projectile
:defer t
:init
(setq projectile-project-search-path '("~/projects/" "~/work/" "~/playground"))
:config
;; I typically use this keymap prefix on macOS
(define-key projectile-mode-map (kbd "s-p") 'projectile-command-map)
;; On Linux, however, I usually go with another one
(define-key projectile-mode-map (kbd "C-c C-p") 'projectile-command-map)
(global-set-key (kbd "C-c p") 'projectile-command-map)
(projectile-mode +1))
This is going to be useless, though, as projectile-mode
will run at the end of the :config
block
forcing the package to be loaded. We can make things a bit better if we instruct the mode to be loaded
only after Emacs’s initialization has finished:
(use-package projectile
:defer t
:init
(setq projectile-project-search-path '("~/projects/" "~/work/" "~/playground"))
:config
;; I typically use this keymap prefix on macOS
(define-key projectile-mode-map (kbd "s-p") 'projectile-command-map)
;; On Linux, however, I usually go with another one
(define-key projectile-mode-map (kbd "C-c C-p") 'projectile-command-map)
(global-set-key (kbd "C-c p") 'projectile-command-map)
:hook (after-init . projectile-mode)
Note that we’re using the name after-init
instead of after-init-hook
, as the hook is actually
named. That’s done to spare you some typing, but I understand it might also be a bit confusing. You
can enforce the usage of the full hook names like this:
(setopt use-package-hook-name-suffix nil)
So, what can we improve next? Ideally we should get rid of :defer
, :init
and :config
:
(use-package projectile
:custom (projectile-project-search-path '("~/projects/" "~/work/" "~/playground"))
:bind-keymap (("C-c C-p" . projectile-command-map)
("C-c p" . projectile-command-map)
("s-p" . projectile-command-map))
:hook (after-init . projectile-mode)
Much better!
Of course, you can’t always achieve this clean setup, but if you try you’ll get there 90% of the time!
So, to recap:
- Avoid the use of
:init
,:config
and:preface
whenever possible - Most of the time you don’t need to use
:defer
- Usually you should aim to activate minor modes only after Emacs’s main initialization has finished (otherwse
:defer
is pointless)
I’ll add here that less is more, even in Emacs. It’s usually a good idea to review the list of packages in your .init.el
every few months
and trim it from time to time. I used to be the type of guy who loads 100+ packages in their config, but these days I limit myself only
to packages really improve my workflows.
If use-package
still feels like black magic to you I can suggest the following:
- Macroexpand various
use-package
blocks in your config to see what’s the generated Emacs Lisp code. Here’s an example you can try:
(macroexpand-1
'(use-package projectile
:custom (projectile-project-search-path '("~/projects/" "~/work/" "~/playground"))
:bind-keymap (("C-c C-p" . projectile-command-map)
("C-c p" . projectile-command-map)
("s-p" . projectile-command-map))
:hook (after-init . projectile-mode)))
;; macroexpansion
(progn
(use-package-ensure-elpa 'projectile '(t) 'nil)
(defvar use-package--warning78
#'(lambda (keyword err)
(let
((msg
(format "%s/%s: %s" 'projectile keyword (error-message-string err))))
(display-warning 'use-package msg :error))))
(condition-case-unless-debug err
(progn
(let ((custom--inhibit-theme-enable nil))
(unless (memq 'use-package custom-known-themes)
(deftheme use-package) (enable-theme 'use-package)
(setq custom-enabled-themes
(remq 'use-package custom-enabled-themes)))
(custom-theme-set-variables 'use-package
'(projectile-project-search-path
'("~/projects/" "~/work/" "~/playground")
nil nil
"Customized with use-package projectile")))
(unless (fboundp 'projectile-mode)
(autoload #'projectile-mode "projectile" nil t))
(add-hook 'after-init-hook #'projectile-mode)
(bind-key "C-c C-p"
#'(lambda nil (interactive)
(use-package-autoload-keymap 'projectile-command-map
'projectile nil)))
(bind-key "C-c p"
#'(lambda nil (interactive)
(use-package-autoload-keymap 'projectile-command-map
'projectile nil)))
(bind-key "s-p"
#'(lambda nil (interactive)
(use-package-autoload-keymap 'projectile-command-map
'projectile nil))))
(error (funcall use-package--warning78 :catch err))))
I know this looks a bit intimidating at first, but if you spent a bit of time reading the code you’ll see there’s nothing scary about it.3
use-package
also comes with profiler, you can setuse-package-compute-statistics
to t, restart Emacs and calluse-package-report
to see which packages are taking too much time to set up and what stage they’re at.4
That’s all I have for you today. Feel free to share other use-package
tips in the comments!
-
From my own
init.el
- after I all I told you I don’t really care about the startup time. :D ↩ -
I’ll have to admit I don’t even remember what
:preface
does. ↩ -
You might also want to get wild with something like https://github.com/emacsorphanage/macrostep ↩
-
Thanks to Andrey Listopadov for reminding me about this! ↩