From 91d0d1dd5a3b93add94ae19d1c7d21c9cbc38945 Mon Sep 17 00:00:00 2001 From: Valentin Semirulnik Date: Mon, 11 May 2026 01:31:35 +0400 Subject: [PATCH] perf: skip null-context bind in eachMapping; direct names array read MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three small structural changes to the per-mapping eachMapping loop: 1. Skip `aCallback.bind(context)` when `context === null`. The bind produces a bound function V8 can't inline, so every per-mapping call dispatches through Function.prototype's internal [[Call]] path. With no context (the common case — both bench callsites and most consumers omit it), `bind` is unnecessary and the unbound `aCallback` inlines normally. 2. Read `this._names._array` directly instead of `this._names.at(idx)` per named mapping. Saves the bounds-check + function-call pair — matches the slab-direct pattern used in `_serializeMappings` / `fromSourceMap`. 3. Hoist `mapping.source` / `mapping.name` into locals before building the result object — V8 should CSE this, but explicit locals are slightly more inline-friendly. Bench (`SOLO=1 PHASES=eachmapping-generated,eachmapping-original scripts/bench-diff.sh main trace`, two-process): preact +343%, react +316%, amp +233%, issue-41 +218%, babel.min +214%, vscode +173% — mean +243% across 12 rows. The headline number reflects the bench's NOOP callback amplifying the bind-vs-inline gap. With a real callback that does work the proportional win shrinks, but the structural fix (no bound dispatch when context is null) stands. Baseline error bars on amp are ±8% vs candidate's ±0.5%, consistent with the bound-dispatch path being in a deopt/reopt cycle. --- lib/source-map-consumer.js | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/lib/source-map-consumer.js b/lib/source-map-consumer.js index b8d962ac..963e4240 100644 --- a/lib/source-map-consumer.js +++ b/lib/source-map-consumer.js @@ -173,8 +173,14 @@ SourceMapConsumer.prototype.eachMapping = throw new Error("Unknown order of iteration."); } - var boundCallback = aCallback.bind(context); - var names = this._names; + // Skip the `Function.prototype.bind` allocation when no context was + // supplied — `aCallback || null` defaults to a null context, which is + // the same as calling the function unbound. + var cb = context !== null ? aCallback.bind(context) : aCallback; + // Direct reads of the underlying `_array` slot match the slab-direct + // pattern in `_serializeMappings` / `fromSourceMap`. Skips the `at()` + // bounds-check + function call per named mapping. + var nameArray = this._names._array; // `_absoluteSources` is precomputed in both consumer constructors as // `computeSourceURL(sourceRoot, _sources.at(i), sourceMapURL)`, so a // direct index skips the per-call URL parse + resolve. Same memoization @@ -183,13 +189,15 @@ SourceMapConsumer.prototype.eachMapping = for (var i = 0, n = mappings.length; i < n; i++) { var mapping = mappings[i]; - boundCallback({ - source: mapping.source === null ? null : absoluteSources[mapping.source], + var src = mapping.source; + var nm = mapping.name; + cb({ + source: src === null ? null : absoluteSources[src], generatedLine: mapping.generatedLine, generatedColumn: mapping.generatedColumn, originalLine: mapping.originalLine, originalColumn: mapping.originalColumn, - name: mapping.name === null ? null : names.at(mapping.name) + name: nm === null ? null : nameArray[nm] }); } };