DEV Community

George Fean
George Fean

Posted on Originally published at bundledrop.app

How to Compare React Native Bundles Between Releases

A React Native bundle-size regression usually starts with a disappointingly vague observation:

The bundle got bigger.

Maybe yesterday’s Android production bundle was smaller.

Maybe a pull request added a dependency.

Maybe a framework upgrade changed Metro output.

Maybe one feature imported far more code than expected.

The total size tells you that something changed.

It does not tell you what changed.

That distinction is why I think bundle analysis becomes much more useful when it is treated as a comparison problem, not just a visualization problem.

A treemap can answer:

What is inside this build?

A proper comparison should answer:

What changed between these two builds?

Those are different questions.

Bundle Drop Sight supports both workflows, but the approach itself is useful regardless of which analyzer you use: generate comparable production artifacts, attribute the output through matching source maps, and compare source identities rather than eyeballing two unrelated screenshots. (Bundle Drop)

Start with two complete build pairs

A React Native comparison is not:

old.bundle
vs
new.bundle
Enter fullscreen mode Exit fullscreen mode

Each bundle needs its own matching source map.

Conceptually:

Baseline build
├── production JS bundle
└── matching source map

Current build
├── production JS bundle
└── matching source map
Enter fullscreen mode Exit fullscreen mode

The source map matters because the generated JavaScript bundle no longer maps cleanly to the original TypeScript or JavaScript files by inspection alone.

Metro transforms modules, wraps code, resolves dependencies, and emits generated JavaScript.

The source map connects generated positions back to source identities.

That lets an analyzer ask a narrower and much more useful question:

How many bytes in this generated bundle are associated with each mapped source? (Bundle Drop)

Those numbers describe generated JavaScript ownership.

They are not the file sizes of your original .tsx files, and they are not the size of the final .ipa, .apk, or .aab.

That distinction is important when interpreting the result.

The source map has to match the bundle

Source maps are positional.

A map from one build can still be perfectly valid JSON while being the wrong map for another build.

A tiny code change can shift generated positions throughout a minified bundle.

So the baseline bundle needs the baseline source map.

The candidate bundle needs the candidate source map.

For useful comparisons, also keep the build conditions equivalent:

same platform
same entry point
same Metro configuration
same development flag
same minification setup
Enter fullscreen mode Exit fullscreen mode

If you compare an Android production bundle against an iOS bundle, or a minified build against a differently configured build, the delta may be dominated by the build process instead of the code change you are investigating. (Bundle Drop)

There is one obvious exception: if the Metro or build configuration change is exactly what you are trying to measure, then changing that variable is intentional.

Otherwise, keep the environment boring.

Analyze both builds before comparing them

A useful comparison begins by treating each build as a complete analysis.

For each bundle, generated bytes can be grouped into areas such as:

Application code
Dependencies
Metro / runtime
Unmapped output
Enter fullscreen mode Exit fullscreen mode

This matters because a total delta alone can still hide the interesting part.

Suppose the bundle increased by 150 KB.

That might actually mean:

Application code     +90 KB
Dependencies        +180 KB
Metro/runtime          0 KB
Unmapped             -20 KB
Removed dependency  -100 KB
Enter fullscreen mode Exit fullscreen mode

Now there is something to investigate.

The total change is only the final sum.

The category changes tell you which direction to look first. Sight’s current attribution model keeps application code, dependencies, runtime output, and unmapped bytes explicit instead of silently discarding unattributed output. (Bundle Drop)

Match source identities, not treemap positions

One mistake I would avoid is opening two treemaps side by side and trying to visually spot the difference.

Treemap layouts are relative.

If one package grows, several unrelated rectangles may move or change shape even though their own size barely changed.

Rankings can also move.

So:

baseline screenshot
+
current screenshot
Enter fullscreen mode Exit fullscreen mode

is a weak diff.

A better comparison matches stable identities.

Sight Compare currently analyzes the baseline and current build independently, then matches normalized package and file identities.

For each matched entry it can record:

before bytes
after bytes
signed delta
percentage change
state
Enter fullscreen mode Exit fullscreen mode

where state can be:

added
removed
increased
decreased
unchanged
Enter fullscreen mode Exit fullscreen mode

That makes the investigation much closer to a source diff than a visual guessing exercise. (Bundle Drop)

Sort by absolute change first

When investigating a regression, I would not immediately sort by “largest file.”

The largest package in the application may have existed for years and have nothing to do with the current change.

Instead, start with the largest absolute deltas.

A package moving from:

600 KB → 603 KB
Enter fullscreen mode Exit fullscreen mode

is probably less interesting than a package moving from:

20 KB → 170 KB
Enter fullscreen mode Exit fullscreen mode

even though the first package remains much larger overall.

Likewise, inspect decreases.

Suppose a dependency upgrade adds 200 KB but removes another 180 KB library.

Looking only at additions can make the release look much worse than the actual net change.

A comparison should help you understand the complete movement of bytes rather than produce a blacklist of whatever currently occupies the most space.

Trace the regression down one level at a time

A useful workflow is:

Total bundle delta
        ↓
Category delta
        ↓
Package delta
        ↓
File delta
        ↓
Source / feature being reviewed
Enter fullscreen mode Exit fullscreen mode

Imagine a pull request increases the bundle by 240 KB.

The category view says nearly all of it came from dependencies.

One newly added package accounts for 210 KB.

Inside that package, two modules contribute most of the generated output.

Now the review can ask concrete questions:

Did we intentionally add this dependency?

Are we importing the broad package entry point?

Is a smaller subpath available?

Did the package replace code elsewhere?

Is the cost justified by the feature?

That is a much better discussion than:

“Can we somehow reduce bundle size?”

Bundle analysis should produce investigation paths, not automatic architectural verdicts.

A large dependency may be entirely justified. The useful part is knowing that the cost exists and where it came from. (Bundle Drop)

A practical pull-request workflow

The workflow I would use for a meaningful change is straightforward.

Generate a production baseline from the target branch or last accepted release.

Generate the candidate using equivalent build settings.

Keep each bundle beside its matching source map.

Open both build pairs in Compare mode.

Review the total and category changes first.

Then filter to changed, added, or removed entries.

Inspect the largest absolute package and file deltas.

Search for the feature, package, or source path involved in the pull request.

Finally, export the result if the explanation belongs in the PR or release record. (Bundle Drop)

The goal is to turn:

Bundle increased by 312 KB.

into something closer to:

The feature added application code, introduced package X, removed package Y, and most of the net increase comes from two specific source groups.

That explanation is reviewable.

The raw total is not.

Keep the baseline meaningful

A comparison is only as useful as the baseline.

For pull requests, main or the target branch is often the obvious baseline.

For release engineering, the previous accepted production release may be more useful.

For a large dependency migration, you might intentionally preserve the pre-migration build as a long-lived reference.

What matters is that the baseline represents a state the team understands.

If the baseline itself contains several unrelated pending changes, the comparison becomes harder to interpret.

Bundle-size analysis works best when the question is narrow:

What did this release change relative to the state we intended to compare against?

Export the explanation, not the source map

Source maps can be sensitive engineering artifacts.

They may contain source paths and may include sourcesContent, which can contain original source text.

That is one reason Sight performs its analysis locally in the browser: the selected production bundle and source map are not uploaded to Bundle Drop. The same local-only boundary applies to the analysis and report generation. (Bundle Drop)

The normalized result can then be exported as evidence.

Sight currently supports local reporting formats including PDF, standalone HTML, Markdown, normalized JSON, and relevant image exports. (Bundle Drop)

That can be useful for attaching an explanation to:

a pull request
a release review
a dependency migration
an architecture discussion
Enter fullscreen mode Exit fullscreen mode

But exported reports can still reveal package names, source paths, and size information.

Local analysis reduces the source-code exposure boundary.

It does not make every resulting report safe to publish publicly.

Do not turn bundle bytes into a performance score

Bundle size is useful evidence.

It is not a complete performance model.

A smaller module can execute much more expensive work than a larger one.

Generated-byte attribution does not directly measure:

startup CPU time
memory pressure
rendering cost
runtime network behavior
native binary size
compressed transfer size
Hermes bytecode size
Enter fullscreen mode Exit fullscreen mode

Metro configuration, Babel transforms, platform resolution, and tree-shaking behavior can also change generated output. (Bundle Drop)

So if the application starts slowly, use runtime and native profiling tools.

If the question is dependency graph structure or transformed modules, Expo Atlas may be the more appropriate tool.

If the question is:

Where did these generated JavaScript bytes come from, and what changed since the previous build?

then source-map-based comparison is a good fit. (Bundle Drop)

Analyze and Compare solve different problems

This is the distinction I find most useful.

Analyze asks:

What is inside this build?

That is good for understanding composition, large packages, large application files, and unattributed output.

Compare asks:

What changed?

That is better when a pull request, dependency upgrade, or release unexpectedly moves the total.

Bundle Drop Sight exposes both workflows, but they are intentionally separate because a current-state inspection and a regression investigation are different engineering tasks. (Bundle Drop)

Sight is available here: Open Bundle Drop Sight

It is free, requires no account, and analyzes the selected bundle and source map locally in the browser. (Bundle Drop)

I originally built Sight while working on Bundle Drop’s React Native and Expo release tooling, but the analyzer does not require Bundle Drop OTA and is useful independently of it.

The technical source explaining attribution, matching, comparison, exports, privacy, and limitations is: How to Analyze a React Native Bundle with Source Maps

The useful goal is not automatically making every bundle smaller.

It is being able to explain why it changed before that change becomes a release surprise.

Top comments (0)