diff --git a/test/bench/bench-scan.mjs b/test/bench/bench-scan.mjs
new file mode 100644
index 0000000..065818d
--- /dev/null
+++ b/test/bench/bench-scan.mjs
@@ -0,0 +1,146 @@
+// 掃描層效能基準(jsdom)。度量 render 之 CPU 掃描成本——確定性計數(el.matches() 呼叫數)
+// +壁鐘中位數。jsdom 無版面(getClientRects 回 0),故 lineEdge/gapTrim 不執行、此基準僅覆
+// 蓋 charify/jiya/jinze/spacing/orphan/longWord 之掃描;版面層(S1)另見 bench-layout。
+//
+// 用法:node test/bench/bench-scan.mjs [scale] [iters]
+// scale:段落規模乘數(預設 60);iters:計時重複次數取中位數(預設 9)。
+//
+// 建立兩種壓力結構:寬(多兄弟段落,測線性)+深(巢狀塊測 orphan S2、巢狀行內測祖先游走/
+// insidePill)。計數用正確之 Element.prototype.matches 補丁(非 HTMLBodyElement.prototype)。
+
+import { JSDOM } from "jsdom";
+import { createJuzhen } from "../../dist/juzhen.mjs";
+
+const scale = Number(process.argv[2] || 60);
+const iters = Number(process.argv[3] || 9);
+
+/** 一段混合行內:透明(strong/em/span)、pill(code)、isolate(mark)、opaque(button)、
+ * 標點(句內/句末/括號引號)、中西混排、長英文詞、末尾實義字。 */
+function para(i)
+{
+ return '
第' + i + '段開頭文字'
+ + '粗體巢狀斜體內容與'
+ + '雙層透明codeSample片段'
+ + '及標記字又'
+ + '含 Latin word 與 supercalifragilistic 長詞,'
+ + '句內點號、頓號;冒號:「引號」(括號)測試。末尾實義字甲乙丙丁。
';
+}
+
+/** 深巢狀塊(測 orphan S2 之逐塊 querySelector 掃子樹):div 套 depth 層,內含葉段落。 */
+function deepBlock(depth, inner)
+{
+ let s = inner;
+ for (let d = 0; d < depth; d += 1) { s = '' + s + '
'; }
+ return s;
+}
+
+/** 深巢狀透明行內(測 insidePill/祖先游走:每文本節點沿祖先鏈走 depth 層)。 */
+function deepInline(depth, i)
+{
+ let s = '這是深層巢狀行內的文字內容甲乙丙丁。'
+ + 'codeX中間再一段實義字戊己庚辛。';
+ for (let d = 0; d < depth; d += 1) { s = '' + s + ''; }
+ return '段' + i + s + '
';
+}
+
+function buildHtml()
+{
+ let html = '';
+ // 寬:scale 段普通段落。
+ for (let i = 0; i < scale; i += 1) { html += para(i); }
+ // 深塊:若干「深 8 層 div 包一葉段落」的容器(放大 S2 的 O(Σ塊子樹))。
+ for (let i = 0; i < Math.max(4, scale / 6); i += 1)
+ {
+ html += deepBlock(8, para(1000 + i));
+ }
+ // 深行內:若干「深 10 層 span」的段落(放大 insidePill/祖先游走)。
+ for (let i = 0; i < Math.max(4, scale / 6); i += 1)
+ {
+ html += deepInline(10, 2000 + i);
+ }
+ html += '';
+ return html;
+}
+
+const HTML = buildHtml();
+
+function freshDom()
+{
+ const dom = new JSDOM("" + HTML + "");
+ globalThis.document = dom.window.document;
+ globalThis.NodeFilter = dom.window.NodeFilter;
+ globalThis.Node = dom.window.Node;
+ globalThis.window = dom.window;
+ return dom;
+}
+
+/** 找到 matches 所在原型(Element.prototype),返回 { proto, restore }。 */
+function patchMatches(dom, counter)
+{
+ let p = dom.window.document.querySelector("strong");
+ let proto = Object.getPrototypeOf(p);
+ while (proto && !Object.prototype.hasOwnProperty.call(proto, "matches"))
+ {
+ proto = Object.getPrototypeOf(proto);
+ }
+ const orig = proto.matches;
+ proto.matches = function (s) { counter.n += 1; return orig.call(this, s); };
+ return () => { proto.matches = orig; };
+}
+
+function median(arr)
+{
+ const s = arr.slice().sort((a, b) => a - b);
+ return s[Math.floor(s.length / 2)];
+}
+
+// ---- 計數(單次,確定性)----
+{
+ const dom = freshDom();
+ const counter = { n: 0 };
+ const restore = patchMatches(dom, counter);
+ const jz = createJuzhen();
+ jz.render(dom.window.document.body);
+ restore();
+ const doc = dom.window.document;
+ console.log("結構規模:");
+ console.log(" 文本節點掃描後 jz-char:", doc.querySelectorAll("jz-char").length);
+ console.log(" jz-hws:", doc.querySelectorAll("jz-hws").length,
+ "| jz-jinze:", doc.querySelectorAll("jz-jinze").length,
+ "| jz-orphan:", doc.querySelectorAll("jz-orphan").length);
+ console.log(" DOM 元素總數:", doc.querySelectorAll("*").length);
+ console.log("計數(確定性):");
+ console.log(" render 期間 el.matches() 呼叫數:", counter.n);
+}
+
+// ---- 計時(render 中位數)----
+{
+ const times = [];
+ for (let k = 0; k < iters; k += 1)
+ {
+ const dom = freshDom();
+ const jz = createJuzhen();
+ const t0 = performance.now();
+ jz.render(dom.window.document.body);
+ times.push(performance.now() - t0);
+ }
+ console.log("計時(" + iters + " 次中位數):");
+ console.log(" render:", median(times).toFixed(2), "ms (min",
+ Math.min(...times).toFixed(2) + ")");
+}
+
+// ---- render→revert 往返計時(第三層增量對照之基線)----
+{
+ const times = [];
+ for (let k = 0; k < iters; k += 1)
+ {
+ const dom = freshDom();
+ const jz = createJuzhen();
+ jz.render(dom.window.document.body);
+ const t0 = performance.now();
+ jz.revert(dom.window.document.body);
+ jz.render(dom.window.document.body);
+ times.push(performance.now() - t0);
+ }
+ console.log(" revert+render 全樹往返:", median(times).toFixed(2), "ms");
+}