最近在知乎上回答了一个问题 Emacs启动需要多久,之前一直没怎么花精力去优化启动时间,虽然知道一些理论,但纸上得来终觉浅,于是动手实践。截至发表本文前,优化后的配置运行了三周多,体验和之前无异。
目标
Emacs 中提供了一个函数来记录启动时长,即 emacs-init-time
,后文也用这个时间作为优化目标。
|
|
零配置下启动时间,是优化的终极值。
效果
介绍具体步骤前,先介绍下本次优化的数据
阶段 | (length package-alist) | (emacs-init-time) |
---|---|---|
前 | 116 | 6.50532 |
后 | 65 | 0.943392 |
工具
use-package
我现在的配置都是基于 use-package 来配置,use-package 提供了下面两个配置项:
use-package-verbose
,设置为t
即可打印包加载的信息use-package-minimum-reported-time
,超过这个设定时间会打印耗时,默认是 0.1 秒
有一点需要注意,verbose 统计的是 :config
内的执行时间, :init
的不会统计,所以这个方式统计的时间不一定准确。
benchmark-init-el
本次优化主要使用这个工具,它提供了两种视图:
benchmark-init/show-durations-tabulated
表视图,可以查看一个包以及其依赖的加载时间
|
|
上面这个表示加载 org-contacts 本身需要 10ms ,但是加上其依赖后,总耗时却要 1164ms ,说明其依赖非常重。
benchmark-init/show-durations-tree
树视图,可以查看包的加载顺序,1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
╼►[benchmark-init/root nil 6987ms] ├─[xdg require 14ms] │ ├─[~/.config/emacs/var/autoloads/elfeed-autoloads.el load 46ms] │ ├─[evil-terminal-cursor-changer require 4ms] │ ├─[org-contacts require 10ms] │ │ ├─[org-capture require 17ms] │ │ ├─[org-agenda require 29ms] │ │ │ ╰─[org-refile require 18ms] │ │ ├─[gnus-art require 27ms] │ │ │ ├─[mm-uu require 15ms] │ │ │ │ ╰─[mml2015 require 15ms] │ │ │ ├─[mm-view require 15ms] │ │ │ │ ├─[mml-smime require 15ms] │ │ │ │ ╰─[smime require 21ms] │ │ │ │ ╰─[dig require 14ms] │ │ │ ├─[gnus-sum require 31ms] │ │ │ │ ├─[url require 13ms] │ │ │ │ │ ├─[url-proxy require 17ms] │ │ │ │ │ ├─[url-privacy require 13ms] │ │ │ │ │ ├─[url-expand require 13ms] │ │ │ │ │ │ ╰─[url-methods require 13ms] │ │ │ │ │ ├─[url-history require 17ms] │ │ │ │ │ ╰─[mailcap require 17ms] ...... ......
通过上面的树状图,可以看到 org-contacts 所有依赖的加载时间。
本次优化前的数据放在里这个 gist 中,供读者参考。
自定义 timer
|
|
通过 my/timer
这个宏,可以很方便的测试某段代码的执行时间。
指导思想
- 尽可能懒加载包
- 精简配置,去掉那些华而不实的包,之前很有可能一时兴起安装的包,但是之后再也没用过
优化过程
懒加载所有包
大多数包的安装说明中,都会推荐通过 (xxx-mode 1)
的方式来开启该 mode,这样的优势是简单,用户出问题的机率小,但是带来的一个问题就是会在 Emacs 启动时去加载这些包,即使暂时用不到它。
use-package 提供了 :defer
关键字来支持懒加载,取值如下:
t
,表示不会主动加载这个包- 数字,表示延迟多少秒后加载,内部用
run-with-idle-timer
实现
优化后的配置大部分包均有 :defer t
,然后通过 hook/autoloads 的方式来懒加载,对于其他一些重点需要的包,通过设置延迟时间来优化。比如:
- evil/evil-leader/smex 为 2
- autorevert/so-long/window-numbering 为 5
通过这一步,可以 极大 减少启动时间,也是本次优化最为耗时的部分。在进行实践时,可以通过 benchmark-init 的表视图,找到加载最耗时的包,然后逐个优化。
精简配置
在进行第一步的过程中,发现 projectile 这个包需要 0.7s 的时间,主要时间耗在了 (projectile-mode 1)
这一句上。
|
|
我日常工作流重要依赖项目管理,具体来说有以下三点:
- 可以方便的切换 project
- 可以方便的自定义 project-root ,对于 monorepo 来说尤为重要,而且 lsp-mode/citre 之类的工具也都依赖这个
- project 内搜索文件要快
projectile 我也是调教了很久才用的比较舒服,但感觉还是太重,于是想看看能否用 Emacs 自带的 project.el 来替代它,通过一番搜索,发现 28 版本的 project.el 通过一些简单配置即可满足上述三点需求,于是果断去掉了 projectile 这个依赖。
目前使用的配置可参考:project-config.el,对于 27 版本的用户,可以在这里下载最新的版本。这次去掉的其他华而不实的包主要还有:
- evil-numbers
- company-native-complete
- comment-dwim-2
- carbon-now-sh
- ob-http/ob-sql-mode/org-sidebar/org-bullets
- all-the-icons/all-the-icons-ivy/all-the-icons-dired
- calfw/cal-china-x
- easy-hugo
这些包的特点是:看上去很实用,但基本上没用过,去掉完全不影响使用体验。
重新组织配置文件
通过 benchmark-init 的数据来看,org 相关包占了很大一部分,通过 defer 可以把其相关配置懒加载,但是还有一点容易忽略,即 org-babel。优化前的配置是放在一个大 org 文件中,即所谓的『文学式编程』。
|
|
优化后是拆分到多个 el 文件中,使用 load-file 来加载,之所以选择 load-file,而不是 require 之类的高级 API,是因为它比较底层,黑魔法会少一些。
|
|
file-name-handler-alist
设置为 nil 是参考 2 easy little known steps to speed up Emacs start up time
其他优化
下面列的一些方案本次优化前已经使用,仅供读者参考。以下代码在 early-init.el
中添加:
|
|
总结
Emacs 的启动慢是个老生常谈的问题,但熟练用户的重启机率很小,一般都是 server 模式常驻的,所以启动慢对他们来说并不严重,但是对于新手或其他编辑器阵营的用户来说,启动慢就是一个大瑕疵,希望通过本文的实践能给读者提供优化思路的同时,让更多读者喜欢上把玩 Emacs 。