Byte-compile the `Quickstart’ File for Faster Startup

Long story short: if you would like to have faster startup from your Emacs, enable `quickstart’ feature, and byte-compile your package-quickstart.el. This alone reduced the startup time for my Emacs from ~0.1 seconds to ~0.055 seconds. For the long story read more.

One of important optimizations as described by Hlissner in his Doom Faq was to concatenate autoloads into one big file for faster startup. Since that was written, Emacs has got it’s own implementation of the idea in form of package-quickstart. It seems to help quite a lot. While I was looking at it, I did notice that produced package-quickstart.el could be actually slightly more effective.

What it does is in principle copy entire auoload file as found in each package directory and paste it into one big file to be loaded at Emacs start. Each of those autoload files has code to add it’s directory to Emacs load-path so Emacs can find the autoload file as well as package files. However, when this package-quickstart.el is created, that path is already known. So why should Emacs perform those calculations at startup when we can simply emit those load paths to the file at the time we are emiting it to the disk? Well, I
don’t see any reason why not. It shouldn’t be very hard either.

I did it in somewhat brute-ish manner; I am just taking all the package paths from elpa dir and adding them all to the load-path. It does not take care of activated/not activated packeges but I don’t thing it matters. Here is a simple method to get paths:

(defun get-load-paths()
  (let ((elpa (expand-file-name "elpa" user-emacs-directory))
        (archives (expand-file-name "elpa/archives" user-emacs-directory)) 
        package-paths)
    (dolist (path (directory-files elpa t directory-files-no-dot-files-regexp))
      (when (file-directory-p path)
        (unless (string= path archives)
          (push path package-paths))))
    package-paths))

I could have modified the package-quickstart-refresh itself, but for this purpose I have actually another routine that will edit the produced file:

(defun emit-autoload-paths ()
  (message "Emiting autoloads")
  (package-quickstart-refresh)
  (let ((pp (get-load-paths))
        (pq (expand-file-name "package-quickstart.el"
                              user-emacs-directory)))
    (with-current-buffer (get-buffer-create pq)
      (insert-file-contents pq)
      (goto-char (point-min))
      (forward-line 2)
      (insert (concat "\n(setq load-path (append \n'"
                      (prin1-to-string pp)
                      " load-path))\n"))
      (while (not (eobp))
        (when (char-equal (char-after) ?\^L)
          (delete-char 1))
        (forward-line 1))
      (goto-char (point-min))
      (while (not (eobp))
        (when (re-search-forward "^(add-to-list" (line-end-position) t)
          (forward-line -1)
          (kill-line 3))
        (beginning-of-line)
        (forward-line 1))
      (goto-char (point-min))
      ;; (while (not (eobp))
      ;;   (if (re-search-forward "^(let.*load.*file-name.*" (end-of-line) t)
      ;;       (delete-region (line-beginning-position) (line-end-position))
      ;;     (beginning-of-line))
      ;;   (forward-line 1))
      ;; (goto-char (point-min))
      ;; (while (not (eobp))
      ;;   (when (char-equal (char-after) ?\))
      ;;     (delete-char 1))
      ;;   (forward-line 1))
      ;; (goto-char (point-min))
      (write-file pq nil))))

It probably looks worse than what it is: it just pre-pends some elisp to concatenate pre-calculated package paths with load-path to the package-quickstart.el and it cleans away all statements to add each directory to the load-path. OBS: don’t use this, it just brutishly removes any add-to-list statement found at the beginning of a line. An autoload file might potentially have some other use of add-to-list than to add itself to the load-path. I wrote this just for this measurement and testing, not for the normal use.

This itself seems to reduce startup time for terminal/daemon Emacs from ~0.1 to ~0.055 secs on my machine. GUI Emacs starts in ~0.28 seconds with ~60 packages enabled, which is 0.04 higher then Emacs -Q. I think it is quite good; and with that I think I’ll stop experimenting with optimizing Emacs startup further.

Interestingly enough, I get very similar result if I just byte-compile package-quickstart.el. Also when I enable this, byte compilation seems to add no further speed up. Obviously there is something I don’t really understand with byte-compiled code going on. The long story short; for the most people, one can probably just byte-compile produced package-quickstart.el found in .emacs.d directory to get faster startups. There are probably some good reasones why byte compilation of this file is not enabled by default, so you will probably have to test if it works with your setup.

As seen from the commented out sections, I was also playing with the idea of removing those let-statements out of quickstart file too (I had to manually fix the theme path). It seems to very slightly alter the third decimal place in startup time, which considering potential problems is neither worth it nor advisable.

Update December 18: I was so stupid! 🙂 Statement to disable/enable package-quickstart should be put rather in early-init.el and not in init.el as I did. Now there is a tremendous difference when this file is baked into init file and init file is byte-compiled. Terminal/daemon Emacs start is < 0.02 seconds! GUI Emacs starts in about ~0.24 seconds which is same time I get from stock Emacs (when configured without Gtk and some other extras). If anyone is interested about baking package-quickstart.el, I have updated my Emacs setup on Github.