Repository navigation
Conversation
robhogan
force-pushed
the
pr1984
branch
2 times, most recently
from
September 28, 2026 11:32
4497155 to
5d3015b
Compare
When minifying, the transform worker converted Babel's decoded map to tuples and encoded it for the minifier, then decoded the minifier's output map with `source-map` (`toBabelSegments`), converted it back to tuples and encoded it again as the stored VLQ map. Terser already builds that decoded map, and only encodes it when `result.map` is read. This diff: - Adds an optional `decodedMap` to `MinifierResult`. When a minifier provides it, the worker encodes the stored map straight from it with `vlqMapFromBabelDecodedMap`, as it already does for unminified modules. Otherwise it decodes `map` with `@jridgewell/sourcemap-codec`, which is already installed via `@babel/generator`. - Has `metro-minify-terser` return Terser's `decoded_map` as `decodedMap`, and encode `map` only if it's read. - Passes Babel's own encoded `result.map` to the minifier, rather than re-encoding it from tuples. - Deprecates `MinifierResult.map` in favour of `decodedMap`, and `toBabelSegments`, `toSegmentTuple` and `tuplesFromBabelDecodedMap` in `metro-source-map`, which Metro no longer uses. The next diff removes them. Output is unchanged - across every module of our benchmark app*, the minified code, line count and stored map are byte-identical. Custom minifiers that only return `map` keep working as before. Minifying the whole app is 10.4% faster (95% CI 7.4-11.4%; 21.3s -> 19.2s of CPU on our benchmark app*), from no longer converting the map to and from tuples, and from Terser no longer encoding its map at all: | | Minify step (median) | vs `main` | |---|---|---| | `main` | 21,286ms (20,844 to 21,561) | | | This diff | 19,228ms (18,886 to 19,773) | -10.4% (-11.4 to -7.4) | ## Changelog ``` - **[Performance]**: Faster source map handling when minifying, using the minifier's decoded map if it provides one (`MinifierResult.decodedMap`) - **[Deprecated]**: Custom minifiers should return their source map as `decodedMap` - `MinifierResult.map` is deprecated - **[Deprecated]**: `metro-source-map`: Deprecate `toBabelSegments`, `toSegmentTuple` and `tuplesFromBabelDecodedMap` ``` ## Test plan New tests cover the worker using `decodedMap` without reading `map`, and `metro-minify-terser` returning Terser's decoded map and encoding `map` only on request. ``` yarn jest yarn flow check yarn lint yarn verify-api-snapshots ``` \* Benchmark: Mattermost Mobile 2.45.0 (React Native 0.83.9), its release bundle split back into its 7,082 modules (5.13M mapping segments), each minified with `metro-minify-terser` from its code and map as the worker does. Times are single-threaded totals across all modules - medians of 15 rounds interleaving `main` and both diffs in this stack, Node 22.13.1 on an M5 Pro, with 95% bootstrap confidence intervals. Output was checked byte-identical in every run.
robhogan
marked this pull request as ready for review
November 2, 2026 19:30
robhogan
added a commit
that referenced
this pull request
Nov 2, 2026
…map APIs #1984 added `MinifierResult.decodedMap` and deprecated `MinifierResult.map`, `toBabelSegments`, `toSegmentTuple` and `tuplesFromBabelDecodedMap`. This removes them: - Minifiers return their source map only as `decodedMap`. `MinifierResult.map` is removed, along with the worker's fallback that decoded it with `@jridgewell/sourcemap-codec`, and `metro-minify-terser` now returns only Terser's decoded map. The minifier contract is documented under `minifierPath`. - `toBabelSegments`, `toSegmentTuple` and `tuplesFromBabelDecodedMap` are removed from `metro-source-map`'s exports. Metro no longer uses them, and they have no users in React Native or Expo. A custom minifier (`transformer.minifierPath`) that returns an encoded `map` can return `decodedMap: {mappings: decode(map.mappings), names: map.names}` instead, using `decode` from `@jridgewell/sourcemap-codec`. A minifier that still returns only `map` now fails the build with an error pointing at `decodedMap`, rather than losing its source map. An empty `map`, as in esbuild's `transform` result, still builds with no source map. Anything reading `map` from `metro-minify-terser`'s result directly, which includes `@expo/metro-config`'s transform worker, needs updating too. We'll do that when Expo adopts this release, and its generated types fail to compile until we do. `tuplesFromBabelDecodedMap` stays as an internal helper. `decodeVlqMap`, which expands compact maps when serialising a flat source map (`fromRawMappings`), now decodes with `@jridgewell/sourcemap-codec` into it, rather than through `source-map` via `toBabelSegments`. That drops `source-map.js`'s last use of `source-map` - the package still depends on it for `normalizeSourcePath`. Minified output with Terser is unchanged from #1984, and minification time doesn't measurably change (+1.9%, 95% CI -8.7% to +4.1%*), since that PR already stops Terser encoding its map. Flat maps built from compact maps are about 4.4x faster (95% CI 4.30-4.51x*) and use about 18% less peak memory (95% CI -19.0% to -18.2%), with identical output. Metro's own `.map` responses are index maps, which don't decode compact maps at all, so this only affects the lazy module definition serializer and direct callers of `fromRawMappings`. ## Changelog ``` - **[Breaking]**: Custom minifiers must return their source map as `decodedMap` - `MinifierResult.map` is removed - **[Breaking]**: `metro-source-map`: Remove deprecated `toBabelSegments`, `toSegmentTuple` and `tuplesFromBabelDecodedMap` ``` ## Test plan The worker's mock minifier and `metro-minify-terser`'s tests now use `decodedMap`, tests that used `toBabelSegments` as a reference decoder now use `Consumer`, and tests of the removed functions are removed. A minified release build of our benchmark app* is byte-identical to `main` and to #1984, bundle and source map. New unit tests cover the build error. I also ran a `{code, map}` minifier and `metro-minify-esbuild`'s raw esbuild result through the worker: the first fails with the new error, the second still builds. ``` yarn jest yarn flow check yarn lint yarn verify-api-snapshots ``` \* AI-driven benchmark: Mattermost Mobile 2.45.0 (React Native 0.83.9), its release bundle split back into its 7,082 modules (5.13M mapping segments), each stored as a compact map, then serialised with `fromRawMappings(...).toString()`. Medians of 15 rounds interleaving before and after, Node 22.13.1 on an M4 Pro shared with other work (1-minute load 12-38), with 95% bootstrap confidence intervals. Output was checked identical in every run. Minification times are from the benchmark on #1984, over the same modules.
robhogan
added a commit
that referenced
this pull request
Nov 3, 2026
#1984 added `MinifierResult.decodedMap` and deprecated `MinifierResult.map`. This removes `map`, so minifiers return their source map only as `decodedMap`. The worker's fallback that decoded `map` with `@jridgewell/sourcemap-codec` is removed, and `metro-minify-terser` now returns only Terser's decoded map. The minifier contract is documented under `minifierPath`. A custom minifier (`transformer.minifierPath`) that returns an encoded `map` can return `decodedMap: {mappings: decode(map.mappings), names: map.names}` instead, using `decode` from `@jridgewell/sourcemap-codec`. A minifier that still returns only `map` now fails the build with an error pointing at `decodedMap`, rather than losing its source map. An empty `map`, as in esbuild's `transform` result, still builds with no source map. Anything reading `map` from `metro-minify-terser`'s result directly, which includes `@expo/metro-config`'s transform worker, needs updating too. We'll do that when Expo adopts this release, and its generated types fail to compile until we do. Minified output with Terser is unchanged from #1984, and minification time doesn't measurably change (+1.9%, 95% CI -8.7% to +4.1%*), since that PR already stops Terser encoding its map. ## Changelog ``` - **[Breaking]**: Custom minifiers must return their source map as `decodedMap` - `MinifierResult.map` is removed ``` ## Test plan The worker's mock minifier and `metro-minify-terser`'s tests now use `decodedMap`. New unit tests cover the build error. I also ran a `{code, map}` minifier and `metro-minify-esbuild`'s raw esbuild result through the worker: the first fails with the new error, the second still builds. A minified release build of our benchmark app* is byte-identical to `main` and to #1984, bundle and source map. ``` yarn jest yarn flow check yarn lint yarn verify-api-snapshots ``` \* AI-driven benchmark: Mattermost Mobile 2.45.0 (React Native 0.83.9), its release bundle split back into its 7,082 modules (5.13M mapping segments), each minified with `metro-minify-terser`. Medians of 15 rounds interleaving `main` and this stack, Node 22.13.1 on an M4 Pro shared with other work (1-minute load 12-38), with 95% bootstrap confidence intervals. Output was checked byte-identical in every run.
robhogan
added a commit
that referenced
this pull request
Nov 3, 2026
#1984 added `MinifierResult.decodedMap` and deprecated `MinifierResult.map`. This removes `map`, so minifiers return their source map only as `decodedMap`. The worker's fallback that decoded `map` with `@jridgewell/sourcemap-codec` is removed, and `metro-minify-terser` now returns only Terser's decoded map. A custom minifier (`transformer.minifierPath`) that returns an encoded `map` can return `decodedMap: {mappings: decode(map.mappings), names: map.names}` instead, using `decode` from `@jridgewell/sourcemap-codec`. A minifier that still returns only `map` now fails the build with an error pointing at `decodedMap`, rather than losing its source map. An empty `map`, as in esbuild's `transform` result, still builds with no source map. Anything reading `map` from `metro-minify-terser`'s result directly, which includes `@expo/metro-config`'s transform worker, needs updating too. We'll do that when Expo adopts this release, and its generated types fail to compile until we do. Minified output with Terser is unchanged from #1984, and minification time doesn't measurably change (+1.9%, 95% CI -8.7% to +4.1%*), since that PR already stops Terser encoding its map. ## Changelog ``` - **[Breaking]**: Custom minifiers must return their source map as `decodedMap` - `MinifierResult.map` is removed ``` ## Test plan The worker's mock minifier and `metro-minify-terser`'s tests now use `decodedMap`. New unit tests cover the build error. I also ran a `{code, map}` minifier and `metro-minify-esbuild`'s raw esbuild result through the worker: the first fails with the new error, the second still builds. A minified release build of our benchmark app* is byte-identical to `main` and to #1984, bundle and source map. ``` yarn jest yarn flow check yarn lint yarn verify-api-snapshots ``` \* AI-driven benchmark: Mattermost Mobile 2.45.0 (React Native 0.83.9), its release bundle split back into its 7,082 modules (5.13M mapping segments), each minified with `metro-minify-terser`. Medians of 15 rounds interleaving `main` and this stack, Node 22.13.1 on an M4 Pro shared with other work (1-minute load 12-38), with 95% bootstrap confidence intervals. Output was checked byte-identical in every run.
robhogan
added a commit
that referenced
this pull request
Nov 3, 2026
#1984 deprecated `toBabelSegments`, `toSegmentTuple` and `tuplesFromBabelDecodedMap` in `metro-source-map`. This removes them from the package's exports. Metro no longer uses them, and they have no users in React Native or Expo. `tuplesFromBabelDecodedMap` stays as an internal helper. `decodeVlqMap`, which expands compact maps when serialising a flat source map (`fromRawMappings`), now decodes with `@jridgewell/sourcemap-codec` into it, rather than through `source-map` via `toBabelSegments`. That drops `source-map.js`'s last use of `source-map` - the package still depends on it for `normalizeSourcePath`. Flat maps built from compact maps are about 4.4x faster (95% CI 4.30-4.51x*) and use about 18% less peak memory (95% CI -19.0% to -18.2%), with identical output. Metro's own `.map` responses are index maps, which don't decode compact maps at all, so this only affects the lazy module definition serializer and direct callers of `fromRawMappings`. ## Changelog ``` - **[Breaking]**: `metro-source-map`: Remove deprecated `toBabelSegments`, `toSegmentTuple` and `tuplesFromBabelDecodedMap` ``` ## Test plan Tests that used `toBabelSegments` as a reference decoder now use `Consumer`, and tests of the removed functions are removed. ``` yarn jest yarn flow check yarn lint yarn verify-api-snapshots ``` \* AI-driven benchmark: Mattermost Mobile 2.45.0 (React Native 0.83.9), its release bundle split back into its 7,082 modules (5.13M mapping segments), each stored as a compact map, then serialised with `fromRawMappings(...).toString()`. Medians of 15 rounds interleaving before and after, Node 22.13.1 on an M4 Pro shared with other work (1-minute load 12-38), with 95% bootstrap confidence intervals. Output was checked identical in every run.
robhogan
added a commit
that referenced
this pull request
Nov 3, 2026
#1984 added `MinifierResult.decodedMap` and deprecated `MinifierResult.map`. This removes `map`, so minifiers return their source map only as `decodedMap`. The worker's fallback that decoded `map` with `@jridgewell/sourcemap-codec` is removed, and `metro-minify-terser` now returns only Terser's decoded map. A custom minifier (`transformer.minifierPath`) that returns an encoded `map` can return `decodedMap: {mappings: decode(map.mappings), names: map.names}` instead, using `decode` from `@jridgewell/sourcemap-codec`. A minifier that still returns only `map` now fails the build with an error pointing at `decodedMap`, rather than losing its source map. An empty `map`, as in esbuild's `transform` result, still builds with no source map. Anything reading `map` from `metro-minify-terser`'s result directly, which includes `@expo/metro-config`'s transform worker, needs updating too. We'll do that when Expo adopts this release, and its generated types fail to compile until we do. Minified output with Terser is unchanged from #1984, and minification time doesn't measurably change (+1.9%, 95% CI -8.7% to +4.1%*), since that PR already stops Terser encoding its map. ## Changelog ``` - **[Breaking]**: Custom minifiers must return their source map as `decodedMap` - `MinifierResult.map` is removed ``` ## Test plan The worker's mock minifier and `metro-minify-terser`'s tests now use `decodedMap`. New unit tests cover the build error. I also ran a `{code, map}` minifier and `metro-minify-esbuild`'s raw esbuild result through the worker: the first fails with the new error, the second still builds. A minified release build of our benchmark app* is byte-identical to `main` and to #1984, bundle and source map. ``` yarn jest yarn flow check yarn lint yarn verify-api-snapshots ``` \* AI-driven benchmark: Mattermost Mobile 2.45.0 (React Native 0.83.9), its release bundle split back into its 7,082 modules (5.13M mapping segments), each minified with `metro-minify-terser`. Medians of 15 rounds interleaving `main` and this stack, Node 22.13.1 on an M4 Pro shared with other work (1-minute load 12-38), with 95% bootstrap confidence intervals. Output was checked byte-identical in every run.
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.
When minifying, the transform worker converted Babel's decoded map to tuples and encoded it for the minifier, then decoded the minifier's output map with
source-map(toBabelSegments), converted it back to tuples and encoded it again as the stored VLQ map. Terser already builds that decoded map, and only encodes it whenresult.mapis read.This PR:
decodedMaptoMinifierResult. When a minifier provides it, the worker encodes the stored map straight from it withvlqMapFromBabelDecodedMap, as it already does for unminified modules. Otherwise it decodesmapwith@jridgewell/sourcemap-codec, which is already installed via@babel/generator.metro-minify-terserreturn Terser'sdecoded_mapasdecodedMap, and encodemaponly if it's read.result.mapto the minifier, rather than re-encoding it from tuples.minifierPath.MinifierResult.mapin favour ofdecodedMap, andtoBabelSegments,toSegmentTupleandtuplesFromBabelDecodedMapinmetro-source-map, which Metro no longer uses.Output is unchanged - a release build of our benchmark app* with minification on produces a byte-identical bundle and source map, and so does each module minified on its own. Custom minifiers that only return
mapkeep working as before.Minifying the whole app is about 11% faster, from no longer converting the map to and from tuples, and from Terser no longer encoding its map at all:
mainChangelog
Test plan
New tests cover the worker using
decodedMapwithout readingmap, andmetro-minify-terserreturning Terser's decoded map and encodingmaponly on request.* AI-driven benchmark: Mattermost Mobile 2.45.0 (React Native 0.83.9), its release bundle split back into its 7,082 modules (5.13M mapping segments), each minified with
metro-minify-terserfrom its code and map as the worker does. Times are single-threaded totals across all modules - medians of 15 rounds interleavingmainand both PRs in this stack, Node 22.13.1 on an M4 Pro shared with other work (1-minute load 12-38), with 95% bootstrap confidence intervals. Output was checked byte-identical in every run.