Doom-emacs: Why is doom emacs so fast?

Created on 28 Dec 2017  路  7Comments  路  Source: hlissner/doom-emacs

I try use my own configuration and was amazed by the startup speed of doom eamcs. Why is doom emacs so fast on startup? What optimization did you make?

discussion

Most helpful comment

Doom employs a few techniques to achieve its speed (as well as a ton of premature optimization). Here are some of its more important secrets:

  • The garbage collector eats up a lot of time during startup, so turn up its memory threshold to prevent it from getting triggered:

    (defvar last-file-name-handler-alist file-name-handler-alist)
    (setq gc-cons-threshold 402653184
        gc-cons-percentage 0.6
        file-name-handler-alist nil)
    
    ;; ... your whole emacs config here ...
    
    ;; after startup, it is important you reset this to some reasonable default. A large 
    ;; gc-cons-threshold will cause freezing and stuttering during long-term 
    ;; interactive use. I find these are nice defaults:
    (add-hook! 'emacs-startup-hook
    (setq gc-cons-threshold 16777216
          gc-cons-percentage 0.1
          file-name-handler-alist last-file-name-handler-alist))
    

    (Unrelated to GC, but I also temporarily unset file-name-handler-alist, which Emacs looks through every time it loads a file; this gives me a modest 90-120ms boost).

  • Every time you load or require a package, Emacs does an O(n) equal lookup on load-path. Little you can do about this, but where I can I let-bind load-path to a subset of itself (like in doom-initialize). I also use load! to load all of Doom's module files, which expands to (load ABSOLUTE_PATH nil t) without a load-path lookup.

  • package.el initialization loads every package autoloads file and populates load-path. That makes (package-initialize) a great target for premature optimizers like me! Besides, i don't need those autoloads (I define them manually), so turn it off:

    (setq package-enable-at-startup nil ; don't auto-initialize!
         ;; this tells package.el not to add those pesky customized variable settings
         ;; at the end of your init.el
         package--init-file-ensured t)
    

    And then I populate load-path manually.

  • Lazy load _everything_. Packages are an obvious one; use-package defers 90% of Doom's packages at startup. Package checking isn't done either (as you've noticed, it's done externally), which is a large savings.
  • Exploit byte-compilation. _Many_ expensive things are cached, or even cut out, at compile time. I get a ~40% improvement in startup time with make compile-core. You can byte-compile the whole config with make compile but that takes much longer and you get significantly diminished returns.
  • Use lexical-binding everywhere. Add ;; -*- lexical-binding: t; -*- to the top of your elisp files. This _can_ cause code breakage if your code depends on dynamic variables, but I've written Doom not to.
  • Avoid lambdas by preferring cl-loop or dolist over mapcar and mapc. Lambdas add a little overhead each time they're executed in loops (and the byte-compiler has trouble inlining them; especially when lexical-binding is on!).

Phew! That's not everything, but those are the most important ones. I hope that helps!

All 7 comments

I noticed that all packages are installed before startup. Is it because package checking is expensive?

Yeah it is faster than Prelude and Spacemacs!!!!!!

Doom employs a few techniques to achieve its speed (as well as a ton of premature optimization). Here are some of its more important secrets:

  • The garbage collector eats up a lot of time during startup, so turn up its memory threshold to prevent it from getting triggered:

    (defvar last-file-name-handler-alist file-name-handler-alist)
    (setq gc-cons-threshold 402653184
        gc-cons-percentage 0.6
        file-name-handler-alist nil)
    
    ;; ... your whole emacs config here ...
    
    ;; after startup, it is important you reset this to some reasonable default. A large 
    ;; gc-cons-threshold will cause freezing and stuttering during long-term 
    ;; interactive use. I find these are nice defaults:
    (add-hook! 'emacs-startup-hook
    (setq gc-cons-threshold 16777216
          gc-cons-percentage 0.1
          file-name-handler-alist last-file-name-handler-alist))
    

    (Unrelated to GC, but I also temporarily unset file-name-handler-alist, which Emacs looks through every time it loads a file; this gives me a modest 90-120ms boost).

  • Every time you load or require a package, Emacs does an O(n) equal lookup on load-path. Little you can do about this, but where I can I let-bind load-path to a subset of itself (like in doom-initialize). I also use load! to load all of Doom's module files, which expands to (load ABSOLUTE_PATH nil t) without a load-path lookup.

  • package.el initialization loads every package autoloads file and populates load-path. That makes (package-initialize) a great target for premature optimizers like me! Besides, i don't need those autoloads (I define them manually), so turn it off:

    (setq package-enable-at-startup nil ; don't auto-initialize!
         ;; this tells package.el not to add those pesky customized variable settings
         ;; at the end of your init.el
         package--init-file-ensured t)
    

    And then I populate load-path manually.

  • Lazy load _everything_. Packages are an obvious one; use-package defers 90% of Doom's packages at startup. Package checking isn't done either (as you've noticed, it's done externally), which is a large savings.
  • Exploit byte-compilation. _Many_ expensive things are cached, or even cut out, at compile time. I get a ~40% improvement in startup time with make compile-core. You can byte-compile the whole config with make compile but that takes much longer and you get significantly diminished returns.
  • Use lexical-binding everywhere. Add ;; -*- lexical-binding: t; -*- to the top of your elisp files. This _can_ cause code breakage if your code depends on dynamic variables, but I've written Doom not to.
  • Avoid lambdas by preferring cl-loop or dolist over mapcar and mapc. Lambdas add a little overhead each time they're executed in loops (and the byte-compiler has trouble inlining them; especially when lexical-binding is on!).

Phew! That's not everything, but those are the most important ones. I hope that helps!

This answer is perfect candidate for Doom's wiki - either under FAQ or under new page.
(I know that everyone can edit wiki, but feels little weird to add something that I don't entirely understand)

Your answer is awesome! Thanks 馃槃

@AloisJanicek agreed! Actually, this was lifted (and paraphrased) from a draft for the wiki, so it'll be in there soon.

And done!

Was this page helpful?
0 / 5 - 0 ratings