use-packageがEmacsの標準で組み込まれたことで、簡単にパッケージの利用ができるようになりました。
しかし、パッケージが増えるにつれてemacsの起動時間は遅くなっていきます。
本格的な開発環境を用意するとなると、数十個のパッケージを入れて使うことになります。
私も数えてみると、依存関係でダウンロードしているパッケージも含めて56個も使っていました。
これらを同期的に逐一読み込んでいたのでは時間がかかってしまいます。
もちろん、GUI版を使っていらっしゃる方や一度起動したら終了しない方、emacs-clientを使ってバックグラウンドで起動し続ける方は起動時間はあまり気にならないかと思います。
私はCUI版を使っており、なおかつ頻繁に終了して作業を切り替えます。
なので、起動時間は早ければ早いほどパフォーマンスが上がります。
この記事では、パッケージの読み込みを非同期にして起動時間を早めることを試みます。
Emacsは起動時にパッケージを初期化します。
一般的な設定では、init.elに初期化のコードを記述して利用しています。
個人的に計測したところ、パッケージの初期化も多少時間がかかっていたので、非同期化します。
(setq package-archives
'(("gnu" . "<https://elpa.gnu.org/packages/>")
("melpa" . "<https://melpa.org/packages/>")
("org" . "<https://orgmode.org/elpa/>")))
;; パッケージ管理の初期化
(defun my-package-setup ()
(unless (bound-and-true-p package--initialized)
(require 'package)
(package-initialize)
(run-hooks 'my-package-initialized-hook)))
;; パッケージセットアップを遅延実行する
(defun my-defer-package-setup ()
(run-with-idle-timer 0.05 nil 'my-package-setup))
;; 早期起動時に呼ばれる
(add-hook 'emacs-startup-hook 'my-defer-package-setup)
最初に、package-archives
で利用するパッケージの参照先を登録します。
emacs-startup-hook
でEmacsの初期化が完了したらmy-defer-package-setup
を呼び出します。
my-defer-package-setup
では0.05秒の遅延を入れてパッケージの初期化を行う関数を呼び出します。
微々たる遅延を入れる理由ですが、これがあるとないではファイルを開いた時のコード表示の処理に差が生まれます。
試しにemacs-startup-hook
でmy-package-setup
を直接呼び出してみるとわかるように、パッケージが初期化されてからコードが表示されます。これが体感的に起動時間が遅いと感じる原因となります。
一方で、0.05秒の遅延を入れるとひとまずコードを表示してくれるので、体感的にもかなり早く起動したように感じます。
遅延を入れると、以下のようなステップで起動の処理がなされます。
基本的なファイル読み込みが先に完了
再描画(redisplay)が実行されて基本画面が表示される
その後にテーマやプラグインが段階的に適用される
my-package-setup
では、パッケージの初期化が行われていないかの判定を入れて、真であれば初期化を進めます。また、後々利用する(run-hooks 'my-package-initialized-hook)
もここで宣言します。my-package-initialized-hook
はパッケージの初期化が完了した時のフックになります。
上のステップで、redisplayの話があるように、early-init.elに以下の要素を書いておくとよいでしょう。
;; Suppress flashing at startup
;; inhibit-redisplayとinhibit-messageをtrueに設定することで、
;; Emacsの起動時のメッセージや再描画を抑制します。
;; これにより、起動時の画面のちらつきが抑制されます。
(setq inhibit-redisplay t)
(setq inhibit-message t)
これでパッケージの非同期初期化が可能になります。
ただし、これを行うとuse-package側で弊害が発生します。
つまり、パッケージの初期化が行われていないのに、use-packageはパッケージがロード可能な状態であると認識しているため、エラーになります。
次のセクションでは、そういったuse-package側の設定を解説します。
非同期初期化の場合、対象のパッケージがメジャーモードで使うのかマイナーモードで使うのかで設定が異なります。
メジャーモードの方は、基本的に今までの記述の仕方で対応可能です。
初回読み込み時にだけ、一手間加える必要がありますが、それは後半に解説します。
一方でマイナーモードは明示的にhookを利用して読み込みの処理を書いてあげます。
遅延読み込みでのuse-packageのルール
:ensure tは使わない
use-packageの:ensure t
は、もしもパッケージがなければ、自動的にパッケージをダウンロードしてロードしてくれます。しかし、これも計測してみた結果、たとえ対象のパッケージがダウンロード済みであったとしても、起動にかなりの時間を要していることがわかりました。
なので、:ensure t
は使わないようにします。
常に:defer t
use-packageで:defer t
を設定すると、起動時には読み込まれずに、必要なタイミングで読み込みを行えるようになります。特にマイナーモードで使うパッケージの場合は、上で準備したmy-package-initialized-hook
を使って明示的に起動タイミングを管理します。
:init は極力使わない
:defer t
の場合、:init
に書いた内容がうまく適用されないことがあります。
というのも:init
に書かれた設定はEmacs起動時に適用されます。しかし、そこに書いた内容に対象のパッケージ固有の関数や変数がある場合、:defer t
で読み込まれていない都合上、エラーになることがあります。よって基本的には:config
の方に記載します。
優先度が高いマイナーモード
マイナーモードでもすぐに使いたいものと、後から使えれば良いものがあります。
例えば、私の場合ですと以下のパッケージはすぐに適用してほしいものです
modus-themes
vertico + consult
そのような場合は、パッケージの初期化が終わり次第すぐにhookで呼び出します。
(use-package vertico
:defer t
:hook (my-package-initialized . (lambda () (vertico-mode)))
...)
優先度が低いマイナーモード
マイナーモードで利用するパッケージには、すぐに使わないものもあります。
見た目に関するものはロードが遅くても問題がないことが多いです。例えば、diff-hlやpowerlineは、起動時すぐに必要にはなりません。
それらはlambdaの中でさらにタイマーを使って数秒後に起動させます。
(use-package powerline
:defer t
:hook (my-package-initialized .
(lambda ()
(run-with-timer 1 nil
(lambda ()
(message "[INFO] powerline enabled (1s delay)")
(powerline-default-theme))))))
マイナーモードで利用するパッケージは、どのメジャーモードでもロードすることが多いと思います。
一方で、メジャーモードのパッケージは特定の拡張子のみ有効になれば良いです。
例えば、go-modeであれば、このように設定しています。
(use-package go-mode
:defer t
:mode ("\\\\.go\\\\'" . go-mode)
:hook (go-mode . lsp)
:config
(setq-default tab-width 4) ;; タブの幅を4に設定
(setq go-tab-width 4) ;; go-modeのインデントを4スペースに設定
(setq go-indent-tabs-mode t)) ;; タブを使うように設定
非同期でパッケージが読み込まれる都合上、この書き方ではgoファイルを起動時に読み込むとgo-modeが有効になりません。
かといって、go-modeの設定にマイナーモードと同じようにhookを書くのは、他のメジャーモードの時も評価が走って無駄です。
:hook (my-package-initialized .
(lambda ()
(run-with-timer 1 nil
(lambda ()
(when (and (buffer-file-name)
(string= (file-name-extension (buffer-file-name)) "go"))
(go-mode))))))
多少強引な対応になりますが、バッファをリロードしてメジャーモードを反映する方法をとります。
パッケージの初期化部分の下あたりに以下のコードを加えます。
;; CLI起動時のメジャーモード適用
(add-hook 'window-setup-hook
(lambda ()
(message "window-setup-hook: display-graphic-p=%s, buffer-file-name=%s, major-mode=%s"
(display-graphic-p) (buffer-file-name) major-mode)
(when (and (not (display-graphic-p))
(buffer-file-name))
(message "CLI startup with file detected, scheduling refresh...")
(run-with-idle-timer 0.5 nil
(lambda ()
(message "Executing revert-buffer-no-confirm and font-lock refresh...")
(revert-buffer-no-confirm)
(font-lock-ensure)
(font-lock-flush))))))
肝はこの部分で、バッファを一瞬ロードし直します。するとmodeに記載された拡張子に反応して拡張子に対応したメジャーモードが起動します。
メジャーモードに合わせてハイライトが変わることがあります。tree-sitterを使っているなら、フォントのスタイルも再読み込みさせます。
(revert-buffer-no-confirm)
(font-lock-ensure)
(font-lock-flush)
メジャーモードはパッケージの非同期化による特殊な設定を行わなくても反映されるようになります。
(setq package-archives
'(("gnu" . "<https://elpa.gnu.org/packages/>")
("melpa" . "<https://melpa.org/packages/>")
("org" . "<https://orgmode.org/elpa/>")))
;; パッケージ管理の初期化
(defun my-package-setup ()
(unless (bound-and-true-p package--initialized)
(require 'package)
(package-initialize)
(run-hooks 'my-package-initialized-hook)))
;; パッケージセットアップを遅延実行する
(defun my-defer-package-setup ()
(run-with-idle-timer 0.05 nil 'my-package-setup))
;; 早期起動時に呼ばれる
(add-hook 'emacs-startup-hook 'my-defer-package-setup)
;; CLI起動時のメジャーモード適用
(add-hook 'window-setup-hook
(lambda ()
(message "window-setup-hook: display-graphic-p=%s, buffer-file-name=%s, major-mode=%s"
(display-graphic-p) (buffer-file-name) major-mode)
(when (and (not (display-graphic-p))
(buffer-file-name))
(message "CLI startup with file detected, scheduling refresh...")
(run-with-idle-timer 0.5 nil
(lambda ()
(message "Executing revert-buffer-no-confirm and font-lock refresh...")
(revert-buffer-no-confirm)
(font-lock-ensure)
(font-lock-flush))))))
;; 優先度が高いマイナーモードパッケージ
(use-package modus-themes
:defer t
:hook (my-package-initialized . (lambda () (load-theme 'modus-operandi t)))
:config
;; Add all your customizations prior to loading the themes
(setq modus-themes-slanted-constructs t
modus-themes-italic-constructs t
modus-themes-subtle-line-numbers t
modus-themes-mode-line '(moody borderless)
modus-themes-syntax '(faint)
modus-themes-region 'bg-only
modus-themes-diffs 'deuteranopia
modus-themes-variable-pitch-ui t
modus-themes-bold-constructs nil
modus-themes-region '(bg-only no-extend))
(define-key global-map (kbd "<f5>") #'modus-themes-toggle))
;; 優先度が低いマイナーモードパッケージ
(use-package powerline
:defer t
:hook (my-package-initialized .
(lambda ()
(run-with-timer 1 nil
(lambda ()
(message "[INFO] powerline enabled (1s delay)")
(powerline-default-theme))))))
;; メジャーモードパッケージ
(use-package go-mode
:defer t
:mode ("\\\\.go\\\\'" . go-mode)
:config
(setq-default tab-width 4) ;; タブの幅を4に設定
(setq go-tab-width 4) ;; go-modeのインデントを4スペースに設定
(setq go-indent-tabs-mode t)) ;; タブを使うように設定