@@ -212,7 +212,7 @@ function compiler(options) {
212212 */
213213 function compile ( events ) {
214214 /** @type {Root } */
215- let tree = { type : 'root' , children : [ ] }
215+ let tree = { type : 'root' , children : [ ] , position : undefined }
216216 /** @type {Omit<CompileContext, 'sliceSerialize'> } */
217217 const context = {
218218 stack : [ tree ] ,
@@ -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
@@ -307,6 +315,14 @@ function compiler(options) {
307315 let firstBlankLineIndex
308316 /** @type {boolean | undefined } */
309317 let atMarker
318+ /**
319+ * Insertions accumulated in walk order: each {at, event} maps to an
320+ * `events.splice(at, 0, event)` in the original implementation. Applied
321+ * once at the end so a wide list's per-item O(n) splice cost collapses
322+ * to a single batched rebuild.
323+ * @type {Array<{at: number, event: Event}> }
324+ */
325+ const insertions = [ ]
310326
311327 while ( ++ index <= length ) {
312328 const event = events [ index ]
@@ -413,9 +429,10 @@ function compiler(options) {
413429 lineIndex ? events [ lineIndex ] [ 1 ] . start : event [ 1 ] . end
414430 )
415431
416- events . splice ( lineIndex || index , 0 , [ 'exit' , listItem , event [ 2 ] ] )
417- index ++
418- length ++
432+ insertions . push ( {
433+ at : lineIndex || index ,
434+ event : [ 'exit' , listItem , event [ 2 ] ]
435+ } )
419436 }
420437
421438 // Create a new list item.
@@ -429,17 +446,100 @@ function compiler(options) {
429446 end : undefined
430447 }
431448 listItem = item
432- events . splice ( index , 0 , [ 'enter' , item , event [ 2 ] ] )
433- index ++
434- length ++
449+ insertions . push ( { at : index , event : [ 'enter' , item , event [ 2 ] ] } )
435450 firstBlankLineIndex = undefined
436451 atMarker = true
437452 }
438453 }
439454 }
440455
456+ // Apply all insertions outside the iteration loop so a wide list's
457+ // O(n*k) shift cost collapses to a single batched rebuild.
458+ //
459+ // Two paths: small lists (most documents, including deep nesting) take
460+ // the splice fast path, which avoids the cost of allocating a fresh
461+ // newSub array. Wide lists go through the batched rebuild and use a
462+ // chunked spread to stay under V8's argument-count limit.
463+ if ( insertions . length > 0 ) {
464+ // The fast path costs one splice per insertion, each shifting the
465+ // events suffix by 1; total cost is O(K * suffix). The rebuild path
466+ // costs one allocation, one sort, and one splice replacing the
467+ // whole range; total cost is O(N + K) plus a fixed allocation and
468+ // sort overhead. Below some K the rebuild's constant overhead
469+ // dominates the per-insertion splice work; above some K the fast
470+ // path's K * suffix dominates the rebuild's bounded cost. The
471+ // crossover varies by document shape, so this threshold is a
472+ // balance between deeply nested lists (which favor staying on the
473+ // fast path) and documents whose lists outgrow the constants
474+ // (which favor the rebuild). The chosen value was confirmed by
475+ // sweeping the threshold across representative inputs.
476+ const SMALL_LIST_LIMIT = 8
477+ if ( insertions . length <= SMALL_LIST_LIMIT ) {
478+ // Splice from latest `at` to earliest so unsplice'd positions stay
479+ // valid. Same-`at` groups are reversed compared to walk order, which
480+ // produces the same final ordering the original splice loop did
481+ // (exit before enter at the same insertion point).
482+ let insertion = insertions . length
483+ while ( insertion -- > 0 ) {
484+ events . splice (
485+ insertions [ insertion ] . at ,
486+ 0 ,
487+ insertions [ insertion ] . event
488+ )
489+ }
490+ } else {
491+ insertions . sort ( ( a , b ) => a . at - b . at )
492+ const rangeLength = length - start + 1
493+ /** @type {Array<Event> } */
494+ const replacement = Array . from ( {
495+ length : rangeLength + insertions . length
496+ } )
497+ let writeIndex = 0
498+ let insertionIndex = 0
499+ let sourceIndex = start
500+ while ( sourceIndex <= length ) {
501+ while (
502+ insertionIndex < insertions . length &&
503+ insertions [ insertionIndex ] . at === sourceIndex
504+ ) {
505+ replacement [ writeIndex ++ ] = insertions [ insertionIndex ] . event
506+ insertionIndex ++
507+ }
508+
509+ replacement [ writeIndex ++ ] = events [ sourceIndex ++ ]
510+ }
511+
512+ // V8 limits the number of arguments that can be spread into a
513+ // function call before triggering a stack overflow. Splice into a
514+ // single call when the new sub-array fits comfortably below that
515+ // limit; otherwise empty the range with one splice and refill it
516+ // in chunks so no individual call hits the limit. The chunk size
517+ // matches the threshold micromark-util-chunked uses for its own
518+ // splice helper, which tracks the same V8 constraint.
519+ const SAFE_SPREAD = 10_000
520+ if ( replacement . length <= SAFE_SPREAD ) {
521+ events . splice ( start , rangeLength , ...replacement )
522+ } else {
523+ events . splice ( start , rangeLength )
524+ let chunkStart = 0
525+ while ( chunkStart < replacement . length ) {
526+ const chunkEnd = Math . min (
527+ chunkStart + SAFE_SPREAD ,
528+ replacement . length
529+ )
530+ events . splice (
531+ start + chunkStart ,
532+ 0 ,
533+ ...replacement . slice ( chunkStart , chunkEnd )
534+ )
535+ chunkStart = chunkEnd
536+ }
537+ }
538+ }
539+ }
540+
441541 events [ start ] [ 1 ] . _spread = listSpread
442- return length
542+ return length + insertions . length
443543 }
444544
445545 /**
@@ -1137,19 +1237,31 @@ function compiler(options) {
11371237 // Creaters.
11381238 //
11391239
1240+ // Each node factory pre-declares `position` as undefined as the trailing
1241+ // property. enter() patches it to a real value, but the property already
1242+ // exists, so V8 sees a single hidden class for each node type from
1243+ // construction through the rest of the pipeline. Without the pre-declare,
1244+ // each node took a hidden-class transition when enter() added position.
1245+
11401246 /** @returns {Blockquote } */
11411247 function blockQuote ( ) {
1142- return { type : 'blockquote' , children : [ ] }
1248+ return { type : 'blockquote' , children : [ ] , position : undefined }
11431249 }
11441250
11451251 /** @returns {Code } */
11461252 function codeFlow ( ) {
1147- return { type : 'code' , lang : null , meta : null , value : '' }
1253+ return {
1254+ type : 'code' ,
1255+ lang : null ,
1256+ meta : null ,
1257+ value : '' ,
1258+ position : undefined
1259+ }
11481260 }
11491261
11501262 /** @returns {InlineCode } */
11511263 function codeText ( ) {
1152- return { type : 'inlineCode' , value : '' }
1264+ return { type : 'inlineCode' , value : '' , position : undefined }
11531265 }
11541266
11551267 /** @returns {Definition } */
@@ -1159,13 +1271,14 @@ function compiler(options) {
11591271 identifier : '' ,
11601272 label : null ,
11611273 title : null ,
1162- url : ''
1274+ url : '' ,
1275+ position : undefined
11631276 }
11641277 }
11651278
11661279 /** @returns {Emphasis } */
11671280 function emphasis ( ) {
1168- return { type : 'emphasis' , children : [ ] }
1281+ return { type : 'emphasis' , children : [ ] , position : undefined }
11691282 }
11701283
11711284 /** @returns {Heading } */
@@ -1174,28 +1287,41 @@ function compiler(options) {
11741287 type : 'heading' ,
11751288 // @ts -expect-error `depth` will be set later.
11761289 depth : 0 ,
1177- children : [ ]
1290+ children : [ ] ,
1291+ position : undefined
11781292 }
11791293 }
11801294
11811295 /** @returns {Break } */
11821296 function hardBreak ( ) {
1183- return { type : 'break' }
1297+ return { type : 'break' , position : undefined }
11841298 }
11851299
11861300 /** @returns {Html } */
11871301 function html ( ) {
1188- return { type : 'html' , value : '' }
1302+ return { type : 'html' , value : '' , position : undefined }
11891303 }
11901304
11911305 /** @returns {Image } */
11921306 function image ( ) {
1193- return { type : 'image' , title : null , url : '' , alt : null }
1307+ return {
1308+ type : 'image' ,
1309+ title : null ,
1310+ url : '' ,
1311+ alt : null ,
1312+ position : undefined
1313+ }
11941314 }
11951315
11961316 /** @returns {Link } */
11971317 function link ( ) {
1198- return { type : 'link' , title : null , url : '' , children : [ ] }
1318+ return {
1319+ type : 'link' ,
1320+ title : null ,
1321+ url : '' ,
1322+ children : [ ] ,
1323+ position : undefined
1324+ }
11991325 }
12001326
12011327 /**
@@ -1208,7 +1334,8 @@ function compiler(options) {
12081334 ordered : token . type === 'listOrdered' ,
12091335 start : null ,
12101336 spread : token . _spread ,
1211- children : [ ]
1337+ children : [ ] ,
1338+ position : undefined
12121339 }
12131340 }
12141341
@@ -1221,28 +1348,29 @@ function compiler(options) {
12211348 type : 'listItem' ,
12221349 spread : token . _spread ,
12231350 checked : null ,
1224- children : [ ]
1351+ children : [ ] ,
1352+ position : undefined
12251353 }
12261354 }
12271355
12281356 /** @returns {Paragraph } */
12291357 function paragraph ( ) {
1230- return { type : 'paragraph' , children : [ ] }
1358+ return { type : 'paragraph' , children : [ ] , position : undefined }
12311359 }
12321360
12331361 /** @returns {Strong } */
12341362 function strong ( ) {
1235- return { type : 'strong' , children : [ ] }
1363+ return { type : 'strong' , children : [ ] , position : undefined }
12361364 }
12371365
12381366 /** @returns {Text } */
12391367 function text ( ) {
1240- return { type : 'text' , value : '' }
1368+ return { type : 'text' , value : '' , position : undefined }
12411369 }
12421370
12431371 /** @returns {ThematicBreak } */
12441372 function thematicBreak ( ) {
1245- return { type : 'thematicBreak' }
1373+ return { type : 'thematicBreak' , position : undefined }
12461374 }
12471375}
12481376
0 commit comments