コードハイライト処理をスコープ化・冪等化し、単一要素ハイライトをエクスポート
@37signals/lexxy のコードハイライトヘルパーが、スコープ指定と冪等性ガードを追加し、さらに単一 <pre> 要素向け API を公開しました。これにより、ページ内の多数のコードブロックに対する不要な再走査が排除され、レンダリングパフォーマンスが大幅に改善されます。
背景
従来の highlightCode() はドキュメント全体を走査し、すべての <pre data-language> 要素に対して Prism を再適用していました。ページ内に多数のコードブロックが存在し、同一ブロックが複数回ハイライトされると、コントローラ数 × コードブロック数 の二乗オーダーの計算が走り、メインスレッドが数秒間フリーズするケースが報告されていました。さらに、highlightElement が内部で innerHTML を書き換えるたびに、ホストアプリの MutationObserver が駆動され、余計なコストが累積しました。
この問題は、Turbo の morph や Stimulus の connect() が繰り返し呼び出された際に顕在化し、スケーラビリティのボトルネックとなっていました。
技術的な変更
スコープ指定: highlightCode にオプション引数 root = document を導入し、デフォルトは従来通り全体走査です。指定されたルート以下の要素だけを対象にすることで、highlightCode(this.element) のようにコンポーネント単位で安全に呼び出せます。
@@
-export function highlightCode() {
- const elements = document.querySelectorAll("pre[data-language]")
+export function highlightCode(root = document) {
+ const elements = root.querySelectorAll("pre[data-language]:not([data-highlighted])")
冪等性ガード: <pre> に data-highlighted="true" 属性が付与されている場合はスキップし、再実行時の重複処理を回避します。highlightElement 内でも同属性チェックを行い、二度目以降は即リターンします。
@@
-export function highlightElement(preElement) {
+export function highlightElement(preElement) {
+ if (preElement.dataset.highlighted === "true") return
@@
- preElement.dataset.highlighted = "true"
+ preElement.dataset.highlighted = "true"
単一要素 API の公開: highlightElement をモジュールエクスポートに加え、src/index.js からも再エクスポートしました。呼び出し側が既に <pre> 参照を保持している場合、余計な querySelectorAll が不要になります。
@@
-export { highlightCode } from "./helpers/code_highlighting_helper"
+export { highlightCode, highlightElement } from "./helpers/code_highlighting_helper"
テスト追加: test/javascript/unit/helpers/code_highlighting_helper.test.js にスコープ制限、冪等性、単一要素ハイライトの 3 つのユニットテストを追加し、全 30 件のヘルパーテストがパスすることを確認しました。テストは afterEach フックで DOM をリセットし、各ケースが相互に影響しないようにしています。
+test("highlightCode only walks pre elements within the given root", () => {
+ // ...
+})
+
+test("highlightCode is idempotent — already-highlighted blocks are skipped", () => {
+ // ...
+})
+
+test("highlightElement highlights a single pre element", () => {
+ // ...
+})
バージョン更新: lib/lexxy/version.rb と package.json が 0.9.15.alpha.4 に更新され、リリースノート相当の変更として扱われます。
設計判断
ルート引数によるスコープ化 は、既存の API 互換性を保ちつつ機能拡張できる最小限の侵襲的変更です。新規キーを導入せず、デフォルト引数で全体走査を維持することで、既存コードの動作をそのまま保ちます。これは、広範囲に使用されている highlightCode() の呼び出し箇所を一括置換せずに済む設計です。
data-highlighted 属性の利用 は、DOM に状態を持たせるシンプルな手法です。外部フラグやキャッシュ構造を導入しないため、メモリ使用量が増えず、また CSS セレクタと組み合わせて :not([data-highlighted]) による高速フィルタリングが可能です。
highlightElement のエクスポート は、呼び出し側が要素参照を既に所有しているケース(例: Stimulus の element プロパティ)でのオーバーヘッド削減を意図しています。API の分離により、責務が明確化され、テストやドキュメントの記述が容易になります。
これらの判断は、バックワードコンパチビリティ と パフォーマンス最適化 の二軸でバランスを取った結果であり、コードベース全体への影響を最小限に抑えています。
まとめ
highlightCode にルートスコープと冪等性ガードを追加し、highlightElement を公開したことで、コードハイライト処理は局所的かつ一度だけ実行されるようになりました。結果として、ページロード時のメインスレッド負荷が大幅に低減し、開発者は既存 API をそのまま利用しつつ、必要に応じて細かい制御が可能です。