Skip to content

Register DOM prototype members only when the prototype is used - #117

Merged
FlorianRappl merged 2 commits into
AngleSharp:develfrom
lahma:perf/lazy-dom-prototypes
Jul 26, 2026
Merged

Register DOM prototype members only when the prototype is used#117
FlorianRappl merged 2 commits into
AngleSharp:develfrom
lahma:perf/lazy-dom-prototypes

Conversation

@lahma

@lahma lahma commented Jul 26, 2026

Copy link
Copy Markdown
Contributor

Contains #111 as its first commit, because both touch DomPrototypeInstance's fields. Merging #111 first leaves one commit here.

A prototype is created for every DOM type an assembly exposes - around 170 of them for AngleSharp alone - and its constructor reflects over the whole type tree of that type right away: properties, methods, events and extension methods, on every interface and base class, allocating a ClrFunction pair per member. A document that touches a dozen DOM types still pays for all of them before the first statement runs.

Change

ObjectInstance.Initialize is exactly the hook for this: Jint calls it the first time an object is asked for a property, and marks the instance initialized before calling it, so the registration can use the object normally.

The constructor body moves there verbatim - same order, prototype link still established last, so member registration still sees exactly the properties it saw before (SetMethod and SetExtensionMethods probe HasProperty while the prototype chain is still the default one).

Two places needed adjusting so that laziness actually holds:

  • the prototype link is now set during initialization, so reading it has to initialize too - Object.getPrototypeOf and instanceof reach it without going through a property lookup;
  • every exposed type gets a constructor, and the constructor writes constructor onto its prototype, which would initialize all of them again. That write is deferred and applied at the end of initialization, exactly where the old code performed it.

Effect

The test suite drops from ~20s to ~3s, which is the same work disappearing - every test builds at least one document. I have not put a number on a single document; that is the honest shape of the measurement I have.

Verification

Behaviour is meant to be identical, so I checked the structure rather than a handful of properties: for ten different DOM object kinds (document, element, collection, token list, named node map, node list, window, DOMParser, ...) I dumped every prototype in the chain with its full own-property name set and its Symbol.toStringTag, plus the global object's own property names. The dump is byte-identical to devel.

Two tests added to DomTests for the parts that are easy to get wrong: the prototype chain of an element is complete and correctly tagged, and HTMLDivElement.prototype.constructor === HTMLDivElement (the deferred write).

net462, net472 and net8.0: 124 passed, 0 failed.

🤖 Generated with Claude Code

https://claude.ai/code/session_0179sA2T7HuRfRfSc2JirFik

lahma and others added 2 commits July 26, 2026 20:52
Indexing a DOM collection from script - `document.getElementsByTagName('p')[0]`,
`element.classList[0]`, and everything jQuery builds on top of them - throws a
`TargetInvocationException` wrapping `EntryPointNotFoundException`.

`IHtmlCollection<T>`, `ITokenList` and `IStringList` declare their numeric
indexer as an explicit re-implementation of `IReadOnlyList<T>`'s indexer. A
member declared that way on an interface is private and abstract, so the
`MethodInfo` obtained from the interface's `PropertyInfo` cannot be invoked: the
implementation lives in a different slot and the runtime has no entry point for
the one we hand it.

Resolve the indexer accessor once, when the prototype is built, against the type
the prototype belongs to, and invoke that. Public accessors - the common case,
including every string indexer and the numeric indexers of `INodeList`,
`INamedNodeMap`, `IStyleSheetList` and friends - are used unchanged.

This is what makes the six currently failing tests in `ScriptingTests` and
`JqueryTests` fail; they pass again with this change.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0179sA2T7HuRfRfSc2JirFik
A prototype is created for every DOM type an assembly exposes, and its
constructor reflects over the whole type tree of that type right away -
properties, methods, events, extension methods, on every interface and base
class. A document that touches a dozen DOM types still pays for all of them
before the first statement runs.

`ObjectInstance` already has the hook for this: `Initialize` is called the first
time an object is asked for a property, and Jint marks the instance initialized
before calling it, so the registration can use the object normally. Move the
constructor body there verbatim - same order, same prototype link established
last, so member registration still sees exactly the properties it saw before.

Two places needed adjusting so that laziness actually holds:

* the prototype link is now set during initialization, so reading it has to
  initialize too - `Object.getPrototypeOf` and `instanceof` reach it without
  going through a property lookup;
* every exposed type gets a constructor, and the constructor writes `constructor`
  onto its prototype, which would initialize all of them. That write is deferred
  and applied at the end of initialization, where the old code performed it.

The test suite drops from ~20s to ~3s, which is the same work disappearing:
every test builds at least one document.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0179sA2T7HuRfRfSc2JirFik

@FlorianRappl FlorianRappl left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM!

@FlorianRappl
FlorianRappl merged commit ae38eaf into AngleSharp:devel Jul 26, 2026
5 checks passed
@lahma
lahma deleted the perf/lazy-dom-prototypes branch July 26, 2026 18:43
@FlorianRappl FlorianRappl added this to the v1.0 milestone Jul 26, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants