Understanding use-package Optimisations
There was a recent appearance on HN of an article about improving
Emacs startup time, with a few mentions of use-package
. This was
topical for me as I'd just been through my own yak-shaving exercise
(startup time now down to about 1.5s, and yes this is immaterial
because I run it as a daemon on login).
There were plenty of useful suggestions, but lacking for me was any
discussion of exactly how use-package
works, so this article
attempts to explain the ones I found most useful.
Background: investigating for yourself
By far the simplest tactic is to just see what use-package
is doing:
it is a macro, so you can look at the code generated. I've been using
ielm for convenience.
This is the basic code generated when requiring a package1.
ELISP> (macroexpand '(use-package foo))
(progn
(defvar use-package--warning146
#'(lambda
(keyword err)
(let
((msg
(format "%s/%s: %s" 'foo keyword
(error-message-string err))))
(display-warning 'use-package msg :error))))
(condition-case-unless-debug err
(if
(not
(require 'foo nil t))
(display-warning 'use-package
(format "Cannot load %s" 'foo)
:error))
(error
(funcall use-package--warning146 :catch err))))
Taking out the error handling boilerplate, we're left with:
(if
(not
(require 'foo nil t))
(display-warning 'use-package
(format "Cannot load %s" 'foo)
:error))
In other words, require the package!
Now let's look at some variations.
Efficiencies
All of these are standard start-up hacks, implemented in a nice DSL by use-package.
Deferring
Firstly, let's see the difference when using :defer t
:
ELISP> (macroexpand '(use-package foo :defer t))
(progn
(defvar use-package--warning147
#'(lambda
(keyword err)
(let
((msg
(format "%s/%s: %s" 'foo keyword
(error-message-string err))))
(display-warning 'use-package msg :error))))
(condition-case-unless-debug err nil
(error
(funcall use-package--warning147 :catch err))))
Stripping away the boilerplate and we're left with… nothing! That shouldn't be surprising; we don't load the package, we'll need to take care of that ourselves somehow. Most of the remaining snippets we'll look at concern themselves with different ways of ensuring that a package is loaded when needed, but not at startup.
On its own, :defer t
is mainly useful for packages that will be
loaded by other packages.
Delaying
The :defer
keyword can also take a numeric argument, representing
the number of seconds to wait before loading. Looking at the
expansion:
ELISP> (macroexpand '(use-package foo :defer 1))
;...
(run-with-idle-timer 1 nil #'require 'foo nil t)
;...
In other words, use an idle timer to delay requiring the package for a second. This is useful for large packages that can take a while to load, but that may not be necessary immediately (which is probably most of them, if you're launching from your login profile!). Helm was one such candidate for me.
Autoloading
A large part of the work that use-package does is to defer loading a
package by configuring autoloads. This is a core facility provided by
Emacs to trigger loading of a package only when some function is first
called, and there are a few ways use-package
identifies functions to
use as triggers.
Key bindings
Possibly the most common requirement is to associate a command from a package with a keybinding; this is Emacs after all! Use-package offers a rich and convenient way to do this:
(use-package foo
:bind ("C-cd" . foo-bar))
This associates the interactdive command foo-bar
, assumed to be in
the package foo
, to the key sequence control-c, d. The expansion is
more interesting:
(progn
(unless
(fboundp 'foo-bar)
(autoload #'foo-bar "foo" nil t))
(bind-keys :package foo
("C-cd" . foo-bar)))
In order, this:
-
Checks that the function
foo-bar
isn't already loaded, and assuming it isn't; -
Sets up an autoload (note that we didn't have to configure this ourselves) triggered by the
foo-bar
function; -
Binds the function to the desired key sequence2.
Naturally as a result it does not need to require
the package.
This is the core of use-package; anything that can be identified as
deferable, will have an autoload configured for it.
Modes
Another very common requirement is to associate a package (mode) with
a file-type. Again, use-package
offers a very convenient way to do
this:
(use-package foo
:mode "\\.asdf\\'")
The expansion, again:
(progn
(unless
(fboundp 'foo)
(autoload #'foo "foo" nil t))
(add-to-list 'auto-mode-alist
'("\\.asdf\\'" . foo)))
In other words, set up another autoload, and add it to the
auto-mode-alist
. This of course assumes that the name of the
package is the name of the mode; if our package was still called foo
but our mode was actually foo-mode
, the syntax variant will set up
the autoload and alist for foo-mode
instead:
(use-package foo
:mode ("\\.asdf\\." . foo-mode))
Hooks
Another common scenario is to configure a hook from the package; for
example, to turn on flycheck-mode
from prog-mode-hook
(ie, all
programming languages). Once again use-package
provides convenient
syntax, and by now the expansion should not be a surprise:
(use-package flycheck-mode
:hook prog-mode)
With expansion (note, the syntax saves you the hassle of adding the
-mode
suffix, although this behaviour can be altered)
(progn
(unless
(fboundp 'flycheck-mode)
(autoload #'flycheck-mode "flycheck-mode" nil t))
(add-hook 'prog-mode-hook #'flycheck-mode))
Manually
Finally, you can manually specify commands to autoload. For example, I have a package to generate lorem-ipsum text. It's rarely used and I don't want to bind any of the commands, so I also don't want the package loaded unless I need it.
Your friend here is :commands
, and you can guess the expansion by
now:
(use-package lorem-ipsum
:commands (lorem-ipsum-insert-sentences
lorem-ipsum-insert-paragraphs))
Dependencies and configuration
Now that everything is autoloaded, a secondary step is to ensure that
a package isn't inadvertently loaded by another package. You can use
:defer
here if you know that a package is only used by something
else, for example you want to ensure3 some library is installed,
but only required by packages that needed it.
Sometimes however you want to load an entire package as part of
another package. In this case, you can specify :after
; an example
from my own setup involves dired extensions:
(use-package dired-quick-sort
:after dired)
The expansion this time looks a little different:
(eval-after-load 'dired
'(if
(not
(require 'dired-quick-sort nil t))
(display-warning 'use-package
(format "Cannot load %s" 'dired-quick-sort)
:error)))
The core functionality this time is eval-after-load
, which is
another special form that only executes its body after another package
has loaded.
This same macro is often used in old-school Emacs configurations to
delay configuration of a package until it has been loaded. Not
surprisingly, use-package
has your back here too; this is exactly
what :config
does when used in conjunction with anything triggering
autoloading:
(use-package dired-quick-sort
:commands (dired-quick-sort)
:config (dired-quick-sort-setup))
And the core expansion:
(progn
(unless
(fboundp 'dired-quick-sort)
(autoload #'dired-quick-sort "dired-quick-sort" nil t))
(eval-after-load 'dired-quick-sort
'(condition-case-unless-debug err
(progn
(dired-quick-sort-setup)
t)
(error
(funcall use-package--warning206 :config err)))))
In other words, defer loading the package by setting up an autoload, and then also configure some code to run when the package does finally load.
Summary
Hopefully by now you have a better undersatnding of what use-package
is doing, and how to use it to improve your own startup time:
-
Use
macroexpand
in order to check whatuse-package
is doing; -
Only load packages when necessary, using whatever is appropriate to ensure it will be autoloaded;
-
Set the variable
use-package-verbose
tot
— this prints out each package-load to the*Messages*
buffer, so you can detect if something is being loaded earlier than you would like, and it also warns if something takes too long in loading (more than 0.3s by default). -
(and if you really want to lower that number!) Use idle-timers to defer non-essential packages until after startup.
I actually see a bit more boilerplate because I enable both
use-package-always-ensure
and recently use-package-verbose
, which
is handy for identifying slow-loading packages.
bind-keys
is a separate package but managed as part of
use-package
.
I have use-package-always-ensure
set, or you can manually add
:ensure t
.