利用lazy-load.el按需加载Emacs插件

随着Emacs的插件越来越多,Emacs的启动速度会越来越幔。

Emacs的启动速度慢,主要有两个原因:

  1. 加载时垃圾回收的阈值太小了,导致启动加载插件时触发了垃圾回收,减慢了启动速度
  2. 默认Emacs加载了过多的插件,导致启动的时候浪费了过多的时间去加载插件

解决第一个问题的方法如下:

(let (;; 加载的时候临时增大`gc-cons-threshold'以加速启动速度。
      (gc-cons-threshold most-positive-fixnum)
      ;; 清空避免加载远程文件的时候分析文件。
      (file-name-handler-alist nil))

    ;; Emacs配置文件内容写到下面.

)

上面的代码的目的时,在Emacs加载任何插件之前临时把 gc-cons-threshold 的值设置为最大,避免Emacs启动时触发垃圾回收。 Emacs配置文件加载完毕后,自动恢复变量 gc-cons-threshold 为默认值,避免运行时Emacs占用过多内存。

至于第二个问题的解决方案,就要用到今天我编写的 lazy-load.el 插件了。

2007 ~ 2008这两年期间,我几乎把当时所有的Emacs插件都玩过一遍,当你把几百个插件一股脑的全部默认加载,Emacs的启动时间可以从秒开延长到几分钟。 那时候就在思考怎么把Emacs的启动时间优化到秒开,同时不减少任何插件的使用,最终开发了 lazy-load.el 插件,即使我用 300+ 插件,Emacs启动速度依然是秒开。

原理

lazy-load.el 的原理很简单:

  1. 我们先把我们的 keymap 定义好,每个按键对应的函数都写好
  2. 告诉Emacs函数对应插件的文件名,这样当调用函数时,Emacs知道去哪里加载插件
  3. Emacs默认只加载非常少的插件, 加载完成后,当我们第一次触发按键时才让Emacs去动态按需加载插件和按键对应的函数

因为99%的Emacs插件在单独加载的时候,都可以在1s之内完成,所以相当于99%的插件都在运行时按需加载, 这样就大大减少了Emacs启动时需要加载的插件数量,从而最终达到提升Emacs启动时间的目的。

安装方法

  1. 下载 lazy-load 里面的 lazy-load.el 放到 ~/elisp 目录
  2. 把下面的配置加入到 ~/.emacs 中
(add-to-list 'load-path (expand-file-name "~/elisp"))
(require 'lazy-load)

使用方法

下面这段代码的意思是,第一次按 Alt + g 时,Emacs在 load-path 目录下去找 goto-line-preview.el 这个文件,加载插件并执行 goto-line-preview 这个函数。

(lazy-load-global-keys
 '(("M-g" . goto-line-preview))
 "goto-line-preview")

下面这段代码的意思是,第一次按在 ruby mode 中按 Ctrl + c t 时,Emacs在 load-path 目录下去找 ruby-extension.el 这个文件,加载插件并执行 ruby-hash-syntax-toggle 这个函数。

(lazy-load-local-keys
 '(("C-c t" . ruby-hash-syntax-toggle))
 ruby-mode-map
 "ruby-extension")

很多全局按键默认已经被Emacs占用了,必须先卸载以后才能重新绑定这些全局按键,比如 Ctrl + x, 下面这段代码就是用 lazy-load-unset-keys 卸载默认绑定的全局按键:

(lazy-load-unset-keys '("C-x C-f" "C-z" "C-q" "s-W" "s-z" "M-h" "C-x C-c" "C-\\" "s-c" "s-x" "s-v"))

高级用法

有时候,我们会用一个前缀按键取分类插件中不同的函数,比如我的 sdcv.el 插件的不同函数就可以按照下面的代码用 Ctrl + z 这个按键作为前缀按键, 先按 Ctrl + z ,再按 p 就可以触发 sdcv-search-pointer 函数

(lazy-load-global-keys
 '(("p" . sdcv-search-pointer)
   ("y" . sdcv-search-pointer+)
   ("i" . sdcv-search-input)
   (";" . sdcv-search-input+))
 "init-sdcv"
 "C-z")

对应的 lazy-load-local-keys 也支持最后一个参数传递前缀按键,只不过 lazy-load-local-keys 对应的不是 global-map ,而是插件的 keymap 。

如果Emacs默认就加载了某个插件,而不需要在运行时动态加载,也可以使用 lazy-load-set-keys 函数做单独的按键绑定操作,不用手动写一行行的重复写类似 (define-key keymap key) 的配置

(lazy-load-set-keys
 '(("M-;" . comment-dwim-with-haskell-style))
 haskell-mode-map)

示例

这里有很多 lazy-load 的示例用法

19 个赞

Awesome! Another great lazy-load package solution.:heart_eyes:

用了很多年了,今天没事把它整理出来。

1 个赞

如果插件多的话设置也变得很多吧

这个还好,插件多唯一的加速方案就是这种用的时候再加载的方案。 插件少的时候,用不用这种技术都无所谓。

没人喜欢启动时间巨长。

这个的设计是初次按下快捷键时加载对应的插件吗,那些没有关联快捷键的插件是怎么处理的?

这个和eval-after-load有什么区别呢?

eval-after-load 一般用于某个文件加载或者模式加载以后执行特定的代码,相当于一个自定义hook。 lazy-load 主要是按键动态触发,当快捷键首次按下时动态加载插件并执行响应的函数。

他们的区别是加载的时机,eval-after-load 遵循的是加载特定的文件或模式的时候执行,属于被动触发,lazy-load 是用户按键触发,属于主动触发。

举个例子, 比如 py 文件打开的时候一般都会用 eval-after-load 'python-mode, 但是我们用 magit commit patch 的时机和文件和模式都没有关系,而是用户觉得想创建 commit 的时候就去加载 magit, 这时候用 lazy-load 就非常合适了。

1 个赞

非按键触发的插件加载一般都是文件加载的时候触发的,这时候最简单的方法就是用 auto-mode-alist 配合 autoload 做文件加载时动态触发,可以参考我这方面的配置 lazycat-emacs/init-mode.el at master · manateelazycat/lazycat-emacs · GitHub

1 个赞

这些功能use-package都有了啊: :bind, :defer

可惜我不用 use-package, 原因有二:

  1. 我玩 Emacs 的时候 use-package 还没有开发出来,我那是主要用我自己开发的 auto-install.el
  2. 我觉得 use-package 把很多东西都隐藏起来了,把简单的事情搞复杂了,我就喜欢直接操作API 的感觉,不喜欢用包装一层的东西

也许我太老了吧,哈哈哈

1 个赞

lazy-load 不也是包装一层吗? :joy::joy::joy:

你要理解自己家做的饭和去餐馆吃别人做的饭的区别,而且我那个年代真的没有 use-package 这些东西

1 个赞

不错,使用下面的方式加载配置启动时间从6.3s降低到了4.1s

(let ((gc-cons-threshold most-positive-fixnum)
      (file-name-handler-alist nil))
  (org-babel-load-file "~/dotfiles/emacs/emacs.org"))

我也去试试去

真•秒开,感谢。lazy-load由于插件不多暂时还没用到。

所以,是时候重新搞一遍配置了。哈哈哈。

还是老王这个好,我老了,也无法理解use-package,用这个试试windows上的emacs,希望能有效哈哈

1 个赞

希望老王多多分享经验 以及这种技术性的代码 上次看到一个one-key-xxx也不错

1 个赞

one-key.el 是我十几年前写的插件,那时候是Emacs下第一个分组按键的解决方案,我离开Emacs开发很多年后,后来看大家都用 Hyrda 来实现类似的方案。

one-key.el 因为十几年没维护了,所以不是那么完美,现在只够我自己用,最近也没时间整理它,看看以后有没有时间重新重构一般出来吧,估计功能没有 Hyrdra 强大,只是我自己习惯了。