Skip to content

perf: pre-declare position on delete factory#4

Merged
wooorm merged 1 commit into
syntax-tree:mainfrom
ChristianMurphy:perf/stable-node-shape
Jun 3, 2026
Merged

perf: pre-declare position on delete factory#4
wooorm merged 1 commit into
syntax-tree:mainfrom
ChristianMurphy:perf/stable-node-shape

Conversation

@ChristianMurphy

@ChristianMurphy ChristianMurphy commented May 4, 2026

Copy link
Copy Markdown
Member

Initial checklist

  • I read the support docs
  • I read the contributing guide
  • I agree to follow the code of conduct
  • I searched issues and discussions and couldn’t find anything or linked relevant results below
  • I made sure the docs are up to date
  • I included tests (or that’s not needed)

Description of changes

Problem: the strikethrough handler creates each delete node as a
fresh object literal, then mdast-util-from-markdown's enter() sets
node.position afterwards. V8 treats that late assignment as a shape
change, so every delete node walks through two hidden classes instead
of one. Core nodes already avoid this by declaring position up front;
without the same trick on extension nodes, GFM trees end up mixing
two layouts and downstream tree walks lose monomorphic dispatch.

Goal: declare position on the delete literal at construction so V8
keeps a single hidden class for delete from start to finish.

Changes:

  • lib/index.js: trailing position: undefined on the delete literal
    in enterStrikethrough.

Notes: pairs with the matching core change in
syntax-tree/mdast-util-from-markdown#53;
merged on its own this branch is a no-op. Validated with npm test
(build, format, 100% coverage, 13 of 13 tests in dev and prod).

Bench (local harness, 3-run median-of-medians, GFM tokenizer and
mdast extensions stacked). Numbers below are the parse time delta
versus an unmodified upstream main; positive numbers mean slower,
negative numbers mean faster.

Input: one paragraph of 1000 strike runs. On the baseline this
scenario carries the second-largest mdast-layer share in the GFM
corpus (full 11.0 ms, tokenize-only 5.2 ms; the mdast layer is the
gap between those two).

Effect of pairing this branch with PR #53:

  • PR #53 alone, extensions on upstream main: 14.6 percent slower.
    Trees mix two layouts because delete nodes still take a shape
    change when position is patched in, so downstream tree-walk call
    sites go polymorphic.
  • PR #53 with this branch alongside (and the three sibling extension
    branches): 2.6 percent slower, which sits inside the bench's drift
    floor of 4 to 12 percent (main vs main with no code change in the
    same window).

The paired configuration recovers about 12 percentage points relative
to PR #53 on its own. Without this branch, PR #53 would appear to
regress strikethrough-heavy parses; with it, the parse runs roughly
as fast as upstream main does today.

@github-actions github-actions Bot added 👋 phase/new Post is being triaged automatically 🤞 phase/open Post is being triaged manually and removed 👋 phase/new Post is being triaged automatically labels May 4, 2026
Problem: the strikethrough handler creates each delete node as a
fresh object literal, then mdast-util-from-markdown's enter() sets
node.position afterwards. V8 treats that late assignment as a shape
change, so every delete node walks through two hidden classes instead
of one. Core nodes already avoid this by declaring position up front;
without the same trick on extension nodes, GFM trees end up mixing
two layouts and downstream tree walks lose monomorphic dispatch.

Goal: declare position on the delete literal at construction so V8
keeps a single hidden class for delete from start to finish.

Changes:
- lib/index.js: trailing position: undefined on the delete literal
  in enterStrikethrough.

Notes: pairs with the matching core change in
syntax-tree/mdast-util-from-markdown#53;
merged on its own this branch is a no-op. Validated with npm test
(build, format, 100% coverage, 13 of 13 tests in dev and prod).

Bench (local harness, 3-run median-of-medians, GFM tokenizer and
mdast extensions stacked). Numbers below are the parse time delta
versus an unmodified upstream main; positive numbers mean slower,
negative numbers mean faster.

Input: one paragraph of 1000 ~~strike~~ runs. On the baseline this
scenario carries the second-largest mdast-layer share in the GFM
corpus (full 11.0 ms, tokenize-only 5.2 ms; the mdast layer is the
gap between those two).

Effect of pairing this branch with PR #53:

- PR #53 alone, extensions on upstream main: 14.6 percent slower.
  Trees mix two layouts because delete nodes still take a shape
  change when position is patched in, so downstream tree-walk call
  sites go polymorphic.
- PR #53 with this branch alongside (and the three sibling extension
  branches): 2.6 percent slower, which sits inside the bench's drift
  floor of 4 to 12 percent (main vs main with no code change in the
  same window).

The paired configuration recovers about 12 percentage points relative
to PR #53 on its own. Without this branch, PR #53 would appear to
regress strikethrough-heavy parses; with it, the parse runs roughly
as fast as upstream main does today.
@ChristianMurphy
ChristianMurphy force-pushed the perf/stable-node-shape branch from a058694 to efdc7da Compare May 4, 2026 21:14
@wooorm
wooorm merged commit b0e933b into syntax-tree:main Jun 3, 2026
5 checks passed
@wooorm wooorm added the 💪 phase/solved Post is done label Jun 3, 2026
@github-actions

This comment has been minimized.

@github-actions github-actions Bot removed the 🤞 phase/open Post is being triaged manually label Jun 3, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

💪 phase/solved Post is done

Development

Successfully merging this pull request may close these issues.

2 participants