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:

  1. Checks that the function foo-bar isn't already loaded, and assuming it isn't;

  2. Sets up an autoload (note that we didn't have to configure this ourselves) triggered by the foo-bar function;

  3. 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:

  1. Use macroexpand in order to check what use-package is doing;

  2. Only load packages when necessary, using whatever is appropriate to ensure it will be autoloaded;

  3. Set the variable use-package-verbose to t — 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).

  4. (and if you really want to lower that number!) Use idle-timers to defer non-essential packages until after startup.


1

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.

2

bind-keys is a separate package but managed as part of use-package.

3

I have use-package-always-ensure set, or you can manually add :ensure t.


emacs

1278 Words

2022-05-30 00:00 +0000

comments powered by Disqus