《elisp 每日一练》——Emacs的启动加速

《elisp 每日一练》——Emacs的启动加速

Masimaro
2026-11-03 / 0 评论 / 0 阅读 / 正在检测是否收录...

在前面的一系列博文中,我们接触到了两种能加速Emacs启动的方案:字节编译和 autoload 机制。今天我们从Emacs启动的角度理解它们是如何做到加速的。方便读者日后能更加灵活的使用这套机制。

背景知识

下面我们列举一些 Emacs 启动时的关键步骤,顺序大概是这么个顺序,但是某些我们并不关系的步骤没有列举在里面

首先当然是我们点击 Emacs程序或者在终端输入 emacs 命令时,正式启动Emacs。

early-init.el

从 Emacs 27 之后引入了 early-init.el 。它早于传统的 init.el。它初始化的时机及早,它会在 Emacs 初始化包管理系统(package system)和图形界面(GUI)之前被加载。所以我们可以在它里面对GUI显示(如关闭滚动条、调整 Frame 尺寸)进行配置、优化垃圾回收阈值、接管包管理。早期配置入口来自 init.el。但是它加载的时机比较晚,通常在常规初始化完成之后。对于GUI界面来说,我们如果想要禁用滚动条,如果将代码放在 init.el中,那么它会加载GUI,此时GUI界面上有滚动条,随后又加载 init.el 的代码,发现我们要禁止加载滚动条,所以它又会取消滚动条显示,然后刷新界面。当然这是一个极快的过程,因此有时候我们可能会看到界面有一些闪烁。为了解决这些问题,所以提供了一种更早介入的手段。

既然 early-init.el 初始化的时机较早,有些资源还没准备好,因此使用时不能无脑的将所有 init.el 中的内容都放到 early-init.el 中。例如,此时 GUI 尚未初始化,任何直接依赖图形界面特性的代码(如设置字体、创建复杂的 UI 组件)都无法可靠运行。

包管理

接着Emacs 会激活包管理系统。当然如果你想接管这个过程,可以在 early-init.el 中设置 package-enable-at-startup 为 nil Emacs会跳过包管理这步

Emacs默认的包管理系统会调用 package-activate-all 激活第三方包。它会通过 package-user-dir 中记录的第三方包的目录进行扫描,并激活里面的每一个包。 我们可以通过 package-load-list 设置加载哪些包

对于每一个包,激活过程主要做两件事:

  • 更新 load-path,把该包的目录添加到 Emacs 的 Lisp 搜索路径中, 这样我们后续通过 load 或者 require 时才能找到具体的文件
  • 加载 autoload 文件,读取并执行该包目录下的 *-autoloads.el 文件。这个文件里存储了大量“函数占位符”,告诉 Emacs 某个函数将来需要时该去哪个文件加载。如果不存在 *-autoloads.el 文件就加载包的主代码文件。根据主代码文件中的 require 或者 load 加载里面的子代码文件

如果执行主代码文件,就会经历执行s表达式,展开宏, 为里面的变量分配内存等操作,但是某些情况下有些函数用户可能并不使用,在启动时一股脑的执行会拖慢启动速度。因此一般都会采用 autoload 机制,以便启动时跳过,未来按需加载。

init.el

接着会进入 init.el 文件的加载。

对于 init.el 中的内容,它会依次执行里面的 S-表达式。遇到 load 或者 require 根据 load-path 中记录的目录去寻找对应的文件,并执行。

默认情况下,这部分即使我们在某个配置文件中写下了 autoload 标志,也无法做到启动加速。这是因为emacs 通过执行init.el 或者通过 init.el 中的 require load 加载其他配置时,是在执行 s表达式, 此时的 ;;;###autoload 对lisp计时器来说只是一个注释。真正想做到启动加载还是需要自己生成 autoloads.el 文件。

自己生成的 *-autoloads.el 文件有两种方式:

  • 因为 *-autoloads.el 本质上还是一个elisp文件,所以我们可以自己手写。
  • 利用Emacs 自动扫描为我们生成一个本地的 loaddefs.el 文件

我们可以下面的代码来扫描并加载对应的 loaddefs.el 文件

;; 1. 指定你的本地插件根目录
(add-to-list 'load-path (expand-file-name "~/.emacs.d/lisp"))

;; 2. 定义自动生成 autoload 索引文件的路径
(setq custom-autoload-file (expand-file-name "~/.emacs.d/lisp/my-loaddefs.el"))

;; 3. 如果文件不存在则自动生成
(unless (file-exists-p custom-autoload-file)
  (make-directory (file-name-directory custom-autoload-file) t)
  (loaddefs-generate (expand-file-name "~/.emacs.d/lisp")
                     custom-autoload-file))

;; 4. 在启动时只加载这个轻量级的存根索引文件
(load custom-autoload-file t)

上面代码我们本质上是通过 loaddefs-generate 为目录下 el 文件生成 loaddefs.el 文件。然后利用 load 加载对应的 loaddefs.el 文件。它与前面的 autoloads.el 文件一样,只是记录需要演示加载的函数信息。后面我们将原来 load el 文件改成了 load loaddefs.el 文件。

从这里我们应该能深入的理解 autoload 机制,它并不是什么黑魔法,Emacs也无法做到在 require 或者 load 的时候识别 ;;;###autoload 注释,跳过这类函数的执行。它能做的仅仅是在扫描时识别注释并提取函数信息记录到另一个el文件。后面我们也是通过加载这个新生成的 el 文件达到加速的目的。我们无法通过加载原来的 el 文件加速。

这就导致了一个问题,如果某个启动时必须执行的函数,与标记了 ;;;###autoload 的函数在同一文件下,而我们又恰好对这个文件生成了对应的 loaddefs.el 文件并加载了这个新生成的文件,这会导致这个必须执行的函数无法执行。所以在使用 autoload 机制时需要注意这一点,我们对于这两种函数应该分开放置

Emacs 的新机制

在早期的Emacs,对于我们自己的配置代码,需要手动的生成对应的文件。而在最新的 31 版本出现了 User Lisp Directory 机制。简单来说,它将我们放入 ~/.emacs.d/user-lisp/ 中的个人配置代码实现了自动生成对应的 autoload 配置文件。这样就不用我们操心它的生成问题,我们只需要在注释中加上 ;;;###autoload 标志。甚至我们都不需要显示的使用 load 去加载生成的文件,对于 autoload 机制的函数,Emacs会自动帮我们完成自动按需加载。但是对于那些必须启动就加载的代码,仍然需要使用 require 加载对应文件

总结

本篇我们通过Emacs的启动流程又进一步讨论了启动加速的一些方案:

  • 通过 early-init.el 进行优化配置
  • 通过 early-init.el 接管包管理系统,按真实流程优化
  • 对于第三方package,直接加载autoload 文件,这一步默认由包管理处理,不需要我们操心
  • 对于自己的代码,可以通过 loaddefs-generate 生成对应的文件,通过 load 加载对应的 autoload 文件而非原始的el文件
  • 如果你是Emacs 31 的用户,可以考虑将代码放入到 ~/.emacs.d/user-lisp/ 让Emacs帮我们自动完成延迟加载机制
0

评论 (0)

取消