Skip to content

perf: pre-declare position on footnote factories#2

Open
ChristianMurphy wants to merge 1 commit intosyntax-tree:mainfrom
ChristianMurphy:perf/stable-node-shape
Open

perf: pre-declare position on footnote factories#2
ChristianMurphy wants to merge 1 commit intosyntax-tree:mainfrom
ChristianMurphy:perf/stable-node-shape

Conversation

@ChristianMurphy
Copy link
Copy Markdown
Member

@ChristianMurphy ChristianMurphy commented May 4, 2026

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 footnote handlers build footnoteReference and
footnoteDefinition nodes as fresh literals, then mdast-util-from-
markdown's enter() sets node.position afterwards. V8 treats that
late assignment as a shape change, so each 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 with mixed layouts and tree walks pay a
polymorphic-dispatch cost.

Goal: declare position on both footnote literals at construction so
V8 keeps a single hidden class per node type.

Changes:

  • lib/index.js: trailing position: undefined on the footnoteReference
    literal in enterFootnoteCall.
  • lib/index.js: trailing position: undefined on the footnoteDefinition
    literal in enterFootnoteDefinition.

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, 22 of 22 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: a document with 1000 footnote definitions and 1000 references
to them. On the baseline this scenario carries a modest mdast-layer
share (full 78.0 ms, tokenize-only 62.4 ms) but every footnote node
still pays the shape-change cost without the pre-declare.

Effect of pairing this branch with PR #53:

  • PR #53 alone, extensions on upstream main: 19.5 percent slower.
  • PR #53 with this branch alongside (and the three sibling extension
    branches): 13.0 percent slower, which sits at the upper end of 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 6.5 percentage points
relative to PR #53 on its own. Without this branch, PR #53 would
appear to regress footnote-heavy parses; with it, most of that
apparent regression goes away.

@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 footnote handlers build footnoteReference and
footnoteDefinition nodes as fresh literals, then mdast-util-from-
markdown's enter() sets node.position afterwards. V8 treats that
late assignment as a shape change, so each 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 with mixed layouts and tree walks pay a
polymorphic-dispatch cost.

Goal: declare position on both footnote literals at construction so
V8 keeps a single hidden class per node type.

Changes:
- lib/index.js: trailing position: undefined on the footnoteReference
  literal in enterFootnoteCall.
- lib/index.js: trailing position: undefined on the footnoteDefinition
  literal in enterFootnoteDefinition.

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, 22 of 22 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: a document with 1000 footnote definitions and 1000 references
to them. On the baseline this scenario carries a modest mdast-layer
share (full 78.0 ms, tokenize-only 62.4 ms) but every footnote node
still pays the shape-change cost without the pre-declare.

Effect of pairing this branch with PR #53:

- PR #53 alone, extensions on upstream main: 19.5 percent slower.
- PR #53 with this branch alongside (and the three sibling extension
  branches): 13.0 percent slower, which sits at the upper end of 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 6.5 percentage points
relative to PR #53 on its own. Without this branch, PR #53 would
appear to regress footnote-heavy parses; with it, most of that
apparent regression goes away.
@ChristianMurphy ChristianMurphy force-pushed the perf/stable-node-shape branch from 7bef571 to 4a85102 Compare May 4, 2026 21:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

🤞 phase/open Post is being triaged manually

Development

Successfully merging this pull request may close these issues.

1 participant