You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
walkTokens takes quadratic time because it copies the results array for every token #4120
Describe the bug
When any extension registers walkTokens, parse time grows quadratically with document size. Marked.walkTokens in src/Instance.ts collects the callback results with values = values.concat(...) for every token, and concat copies the whole array each time. On large documents, walking the tokens takes far longer than lexing and parsing combined.
To Reproduce
import{Marked}from'marked';constdoc=(n)=>Array.from({length: n},(_,i)=>`## Heading ${i}\n\nParagraph ${i} with **bold** and \`code\`.\n\n- a\n- b\n`).join('\n');functiontime(marked,src){for(leti=0;i<3;i++)marked.parse(src);conststart=performance.now();for(leti=0;i<10;i++)marked.parse(src);return(performance.now()-start)/10;}constplain=newMarked();constwithHook=newMarked({walkTokens(){}});for(constnof[500,1000,2000,4000]){constsrc=doc(n);console.log(n,time(plain,src).toFixed(1),time(withHook,src).toFixed(1));}
Node 22.21.1, marked 18.0.14:
blocks
parse without walkTokens
parse with a no-op walkTokens
500
4.9 ms
16.8 ms
1000
8.7 ms
74.0 ms
2000
18.6 ms
392.3 ms
4000
38.4 ms
1641.9 ms
Doubling the document multiplies the time by about 4–5 when the hook is present.
We hit this in a live-preview editor that re-parses on every keystroke. Common extensions (marked-highlight, marked-alert, marked-footnote) register walkTokens. In the browser, for a 164 KB document, walkTokens took about 135 ms per parse, and it was the largest single cost per keystroke.
Expected behavior
Walking the tokens should take linear time. Appending in place fixes it: we swapped every values = values.concat(x) for a helper that pushes x (or each item of x when it's an array) onto values. For a 328 KB document, walkTokens dropped from about 607 ms to 6 ms per parse, and the returned values were the same (concat flattens one level, as the helper does). The values.concat in the combined walkTokens function built in use() could get the same change, although its arrays stay small.
Activity
perf(preview): walk marked tokens in linear time
fix: make walkTokens linear by appending values in place (markedjs#4121)
chore(release): 18.1.1 [skip ci]
perf(preview): use marked's walkTokens now that it is linear
perf(preview): use marked's walkTokens now that it is linear
perf(preview): use marked's walkTokens now that it is linear