As an Emacs configuration grows, startup time can gradually increase. Measuring that increase accurately makes it easier to identify regressions. The majority of Emacs users use emacs-init-time. However, this built-in function does not measure all the work performed during startup.
Emacs startup proceeds through several stages: it loads the early init and regular init files, processes command-line options, sets after-init-time, runs emacs-startup-hook, performs additional initial-frame setup such as applying frame parameters from the configuration, and then runs window-setup-hook after the initial frame parameters have been configured. This makes window-setup-hook a useful place to record a broader startup-time measurement.
For a more accurate startup-time measurement, add the following Emacs Lisp code to early-init.el or init.el:
;; Emacs startup proceeds through several stages: it loads the early init and
;; regular init files, processes command-line options, sets after-init-time,
;; runs emacs-startup-hook, performs additional initial-frame setup such as
;; applying frame parameters from the configuration, and then runs
;; window-setup-hook after the initial frame parameters have been configured.
;; This makes window-setup-hook a useful place to record a broader startup-time
;; measurement.
;;
;; URL: https://www.jamescherti.com/measuring-emacs-startup-time/
(defvar my-recorded-startup-time-message nil
"Stores the formatted string of the Emacs startup metrics.")
(defun my-record-startup-time ()
"Calculate and record the elapsed startup time.
This function records the time when `window-setup-hook' runs."
(setq my-recorded-startup-time-message
(format "Emacs loaded in %.2f seconds (Init time: %.2fs) with %d garbage collections."
(float-time (time-since before-init-time))
(float-time (time-subtract after-init-time before-init-time))
gcs-done))
;; Output to the *Messages* buffer during the initial launch
(message "%s" my-recorded-startup-time-message))
(defun my-display-startup-time ()
"Display the previously recorded Emacs startup time in the echo area."
(interactive)
(if my-recorded-startup-time-message
(message "%s" my-recorded-startup-time-message)
(message "Startup time was not recorded.")))
;; Read startup summary:
;; https://www.gnu.org/software/emacs/manual/html_node/elisp/Startup-Summary.html
(add-hook 'window-setup-hook #'my-record-startup-time 99)
This provides a broader measurement than M-x emacs-init-time because it continues measuring after after-init-time has been set, including subsequent startup processing through window-setup-hook.
A depth of 99 places the function near the end of window-setup-hook. This makes the measurement less likely to omit work performed by other functions on the same hook.
The variable gcs-done tracks the total number of garbage collections during the session. Because this function formats and saves the message string during window-setup-hook, it locks in the exact number of garbage collections triggered during the startup phase, giving you a metric of memory allocation bottlenecks during initialization. Ideally, it should be 0 or 1.
Capturing the time the UI finishes rendering provides a reliable metric to track down performance regressions. Once Emacs has finished loading, you can retrieve the recorded startup time at any point during your session. Execute M-x my-display-startup-time to print the exact initialization metrics in the echo area. This interactive command ensures the startup data remains accessible even if the initial message is cleared from the screen by subsequent buffer activity.
But why do we care? I start my emacs once every two months or so.. unless I don't need to reboot or turn the macine off, emacs is running as a demon
Hello Christian,
That is a fair point. If you rely on a daemon and only restart every couple of months, raw startup time won't really impact your daily usage.
However, I still treat startup speed as a key indicator of overall configuration health. I've optimized mine down to 0.23 seconds alongside 151 packages, and I've found the benefits extend well beyond the initial cold boot.
For example, whenever I am modifying my Elisp configuration or working on package development, I need to restart Emacs frequently to test changes in a clean state. Waiting several seconds for every reload slows down the debugging process.
More importantly, initialization time acts as a reliable canary for regressions. A slow startup usually indicates an over-reliance on eager evaluation rather than properly using autoloads and deferred execution. Enforcing a fast startup keeps the configuration modular and keeps the baseline memory footprint low. If my startup time suddenly spikes, it immediately alerts me to underlying issues, like blocking network requests, synchronous shell calls, or garbage collection bottlenecks, that I might otherwise overlook.
Finally, daemons are not always practical in every environment. When jumping into a quick SSH session on a remote server, it is helpful to have Emacs open instantly without needing to manage background processes.
If you're keen, you can log the different phases, something like:
Init time was 1.54s comprising:
- 0.02s early-init.el
- 0.04s elpa (package-activate-all)
- 0.22s GUI / frame / site-start
- 0.82s init.el
- 0.07s after init.el / after-init-hook
- 0.36s continued startup / emacs-startup-hook
- 0.00s final startup / window-setup-hook
I don't actually care that much about my start-up time; it's just that a few years ago I realised that it had increased dramatically (from about 3s to more than double that) since I'd last paid any attention to it, and I wanted to know what was going on, so I introduced a bunch of extra reporting to give me a better understanding of where the time was being spent.
If you want a more granular breakdown, you can use the benchmark-init package.
It profiles the time spent loading packages and executing configuration blocks during Emacs startup, and it presents the results in either a tabular or tree format.
Here is how you can set it up:
Restart Emacs, then evaluate: