Skip to content

Commit 42ca832

Browse files
perf(compile): reuse dispatch handler context
The compile loop calls Object.assign({sliceSerialize: ...}, context) once per event so each handler's this-binding has a fresh sliceSerialize plus all the shared context fields. For a typical document this is one allocation per event in the hot loop. For larger inputs (~140k events at the 564 KB scale) that is tens of thousands of short-lived merged objects per parse, plus the per-allocation hidden-class transitions. The fix builds the merged this-binding once outside the loop and mutates only its sliceSerialize field per event. Stack, tokenStack, data, config, and the enter/exit/buffer/resume helpers are still the same references that context exposes, so any handler reading or mutating those still sees the right object. Existing handlers do not capture this across events, so a single shared callContext is observably equivalent to the old per-event merge. Inputs that benefit, with multi-run median-of-medians vs the baseline (spread in parentheses): 10,000 character entity references -43.7% (6.5%) 1,000 inline links in a paragraph -37.1% (6.4%) 10,000 short backtick code spans -32.6% (4.1%) CommonMark spec * 35 (~564 KB) -13.4% (3.3%) CommonMark spec * 7 (~113 KB) -10.1% (7.1%) one CommonMark example -9.1% (8.8%) full CommonMark spec (~16 KB) -5.4% (9.2%) Every input that drives a non-trivial number of events through the dispatch loop benefits, because the per-event Object.assign is the cost being eliminated. Inputs with high node-count-per-byte (many small inline tokens) gain the most. Single-run full corpus shows wins between -16% and -39% on a 1,000-fenced-code-block input, a 5,000-ATX-heading input, a 5,000-HTML-block input, a 1,000-image input, a 5,000-tab-heavy input, and a 1,000-autolink input. Trade-offs and inputs where the gain is small or absent: A 10,000-unmatched-asterisks input and a 256 KB Unicode-heavy input both moved within +/- 1% of baseline. Their event count per byte is low, so the per-event Object.assign was not a hotspot. A 1 MB single paragraph and a 5,000-reference-link-definition input each shifted a few percent on a single run; both sit inside their own noise bands. The pure emphasis stress inputs ('a**b' repeated 10,000 times and similar) reported +30% to +44% but their cross-run spread is 47 to 53% on this stack alone, and the input shape (mostly attentionSequence events that mostly do not match a handler) means the per-event merge was not the cost driver. The regression there is noise. Tests pass: dev + prod 1448/1448, mdast-util-gfm 54/54, mdast-util-mdx 11/13. The two failing mdx tests reproduce on upstream/main and are not introduced by this branch.
1 parent f9ef1b3 commit 42ca832

1 file changed

Lines changed: 18 additions & 10 deletions

File tree

dev/lib/index.js

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -247,17 +247,25 @@ function compiler(options) {
247247

248248
index = -1
249249

250+
// The handler this-binding receives the same fields as `context` plus a
251+
// per-event `sliceSerialize`. Pre-allocate the merged object once outside
252+
// the loop and mutate sliceSerialize per event instead of allocating a
253+
// fresh merged object per event. Stack/tokenStack/data references stay
254+
// shared with `context`, so mutations inside handlers remain visible to
255+
// the post-loop code via `context.tokenStack`.
256+
const callContext = /** @type {CompileContext} */ (
257+
/** @type {unknown} */ (
258+
Object.assign({sliceSerialize: undefined}, context)
259+
)
260+
)
261+
250262
while (++index < events.length) {
251-
const handler = config[events[index][0]]
252-
253-
if (own.call(handler, events[index][1].type)) {
254-
handler[events[index][1].type].call(
255-
Object.assign(
256-
{sliceSerialize: events[index][2].sliceSerialize},
257-
context
258-
),
259-
events[index][1]
260-
)
263+
const event = events[index]
264+
const handler = config[event[0]]
265+
266+
if (own.call(handler, event[1].type)) {
267+
callContext.sliceSerialize = event[2].sliceSerialize
268+
handler[event[1].type].call(callContext, event[1])
261269
}
262270
}
263271

0 commit comments

Comments
 (0)