Build the constructor of an exposed type when script first names it - #121
Merged
Merged
Conversation
Every exposed type got a constructor object as the engine was set up: 178 of them per engine, each with its own prototype object, toString function and property descriptors. A document names a handful of them. The property is still registered for every type, with the attributes it had, so nothing about enumerating the window changes - only the object behind it waits for the first read. The prototype keeps the constructor, so reading the name off the window and reading "constructor" off an instance still arrive at the same object; a prototype reached through an instance asks for its constructor itself, since that path never names the type.
This was referenced Jul 27, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Follow-up to the recent perf batch, and the last structural item from #72.
Setting up an engine gave every exposed type a constructor object: 178 of them, each with its own prototype link,
toStringfunction and property descriptors. A document names a handful. The reflection deciding what to build was already cached per type byCreatorCache; the objects built from it are engine-affine and were not.The property is still registered for every type, with the attributes it had —
PropertyFlag.OnlyEnumerableis bit-for-bit whatnew PropertyDescriptor(value, false, true, false)produced — so enumerating the window is unchanged. Only the object behind the property waits for the first read.constructorhad to moveDomConstructorInstance's ctor is what installsconstructoron the type's prototype. Deferring naively would makescreen.constructorundefined, because an object can reach script without its global name ever being read. So the prototype now owns its constructor and pulls it in at the end ofInitialize(); reading the name off the window and readingconstructoroff an instance still arrive at the same object.AddConstructorFunctionsandAddInstancesstay eager on purpose — they cover one type each (Image,Screen), so deferring them would add paths for no measurable gain.Numbers
The reported loop from #72 — 100 engines, all retained — net8.0, workstation GC, serial alternating runs:
For 100 documents that actually run a script (build 50 elements,
querySelectorAll,new CustomEvent,dispatchEvent): 8.28 → 7.68 ms, 735.0 → 527.2 KB per instance. The absolute saving is the same as in the bare case — a real page names two or three constructors, so the win does not erode.Two things this does not do, stated plainly:
PropertyFlag.CustomJsValuecannot be cleared after materialization from outside Jint, and self-replacing the descriptor would mutate the property dictionary whileObject.values(window)iterates it. Measured worst case, 2M reads ofHTMLDivElement: 82.4/81.1 ms before vs 83.3/80.9 ms after — no regression, but it is a real property of the mechanism.PropertyFlag.CustomJsValueis the hook Jint documents as surviving its read inline caches (UnwrapJsValuere-reads the flag on every access rather than snapshotting the value); overridingGetwould not have been safe. Verified against the 4.14.0 tag, which is what this package consumes.Tests
133/133 → 151/151 on net8.0, net462 and net472, warning-clean on all four TFMs.
The 18 new tests in
DeferredConstructorTestsalso pass against unmodifieddevel— they pin existing contract rather than asserting the new behaviour into existence. Separately, a fingerprint over all 180 window own-property names, all 373globalThisnames,Object.keys/for...incounts, descriptor attributes, identity,instanceof,typeof, delete/assign semantics and.constructorfor 20 different objects is byte-identical before and after.