Skip to content

Six React Library Majors, Measured Before and After

Measured before-and-after bundle sizes and the real breaking changes for six React libraries that shipped a major version, with migration steps.

· · 6 min read

Quick Take

We keep a catalogue of React UI packages and re-measure it rather than copying numbers from READMEs. Doing that in August 2026 surfaced six packages that had crossed a major version, so we installed both sides of every jump, bundled them the same way, and read the release notes. One package got 62% smaller. One more than doubled.

TL;DR: Six React packages in our catalogue crossed a major version. We measured both sides of every jump with the same pipeline. react-dropzone shrank 62%, and we pinned the drop to the exact release that caused it. @tanstack/react-table grew 114%. The rest moved between 4% and 26%.

Why measure both sides

Most upgrade advice tells you what broke. Almost none tells you what it cost, because that means installing the old version too, and nobody keeps the old version around.

We maintain a catalogue of React UI packages where every figure is fetched rather than typed. Re-measuring it in August 2026 threw up an obvious follow-up question: six of these packages had crossed a major since the last check, so what did the upgrade actually change on disk?

Every number below comes from the same pipeline. Install the version alone, bundle export * from '<package>' with esbuild in esm, browser, minified, production mode, mark React external because your app already has it, gzip at level 9. That measures the whole public surface, so it is a ceiling rather than a typical cost. It is also identical on both sides of each jump, which is what makes the delta mean something.

The six jumps

PackageFromToBeforeAfterChange
react-dropzone14.3.520.1.116.7 kB6.3 kB-62%
sonner1.7.42.0.89.3 kB9.6 kB+4%
recharts2.15.13.10.1134 kB158 kB+18%
lucide-react0.475.01.34.0144 kB172 kB+19%
react-resizable-panels2.1.74.12.39.1 kB11.4 kB+26%
@tanstack/react-table8.21.29.1.215.3 kB32.7 kB+114%

react-dropzone: six majors, and a 63% drop in one of them

This is the interesting one. Fourteen to twenty is six major versions, and the package came out substantially smaller. Measuring each step shows the drop is not gradual:

VersionSize
14.3.516.7 kB
15.0.016.9 kB
16.0.015.5 kB
17.0.014.9 kB
18.0.05.5 kB
19.0.05.8 kB
20.1.16.3 kB

Almost all of it lands in a single release, and the v18 notes say why: it upgraded file-selector to v4, and "the full extension-to-MIME table is no longer bundled". A lookup table of every known file extension is exactly the kind of thing that is large, rarely needed in full, and invisible until someone measures.

That has a migration cost. If you relied on the extension-to-MIME mapping, you now pass it in yourself:

import { COMMON_MIME_TYPES } from 'file-selector/mime';
import { fromEvent } from 'file-selector';

<Dropzone getFilesFromEvent={event => fromEvent(event, COMMON_MIME_TYPES)} />

v18 also tightened types. FileWithPath now requires path and relativePath, so a plain File is no longer assignable and onDrop handlers typed as FileWithPath[] stop compiling. Type them as File[] instead:

// before
const onDrop = useCallback((accepted: FileWithPath[]) => { /* ... */ }, []);

// after
const onDrop = useCallback((accepted: File[]) => { /* ... */ }, []);

Two more steps have behaviour changes worth knowing. v19 stopped rejecting a whole batch when it exceeded maxFiles, and now accepts the files that fit, so onDropAccepted fires with a partial set where it previously fired with nothing. v20 is Node-only: minimum supported Node is now 22, which affects your CI image and nothing in the browser.

@tanstack/react-table: v9 stable, and twice the size

The largest relative growth in the set. v9 is a stable release of a long-running rewrite, and the release notes point at a per-framework migration guide rather than listing changes inline, which is a fair signal of how much moved.

Put 114% in perspective before reacting to it. The package went from 15.3 kB to 32.7 kB gzipped at full surface. That is real, and it is also small next to almost any application bundle. This is a growth worth understanding, not a growth worth blocking an upgrade over.

recharts: state moved, and Customized lost its props

Recharts 3 rewrote state management. The consequences are narrow but sharp: CategoricalChartState, which gave you access to recharts internals, no longer arrives in event handlers, and <Customized /> no longer receives chart state or props.

If your charts are declarative, you will likely notice nothing. If you built a custom overlay by reading internal state out of Customized, that is the code to rewrite, and the migration guide in the project wiki is where the replacement APIs are listed.

react-resizable-panels: ESM-only, then renamed to match ARIA

Two majors, each with a distinct kind of break.

v3 made the package ESM-only. That is a tooling change, not a code change, and it either does nothing to you or it stops your build outright depending on how your bundler and Jest configuration handle ESM dependencies.

v4 is a rename, and the reasoning is stated plainly: the names now follow web standards. PanelResizeHandle became Separator to match the ARIA separator role, and direction became orientation to match aria-orientation.

// v3
<PanelGroup direction="horizontal">
  <Panel />
  <PanelResizeHandle />
  <Panel />
</PanelGroup>

// v4
<PanelGroup orientation="horizontal">
  <Panel />
  <Separator />
  <Panel />
</PanelGroup>

v4 also widened size constraints to accept pixels, percentages and rem/em rather than percentages alone, which is the feature the rename came packaged with.

lucide-react: check you are not on 1.0.0

A footnote that will cost somebody an afternoon. Lucide's 1.0.0 release carries a warning from the maintainers that it was published unintentionally, and that 1.0.1 should be used instead. If a lockfile pinned 1.0.0 exactly, move it.

The 19% growth from 0.475.0 to 1.34.0 is mostly icons: the set keeps expanding, and the full-surface measurement counts every one of them. This is the entry where the ceiling is least like your real cost, because almost nobody imports the whole icon set. Three icons cost roughly three icons.

sonner: the quiet one

One to two moved 4%, from 9.3 kB to 9.6 kB, and the release notes describe it as mostly bug fixes. Of the six, this is the jump least likely to need any work, which is worth saying out loud: a major version number is a promise that something could break, not evidence that something did.

What we could not verify

framer-motion crossed from 12 to 13 in the same window. We measured 13.1.1 at 64.0 kB, but could not retrieve release notes for that major from the repository, and the 12.4.11 install failed in our harness, so there is no delta either.

Rather than reconstruct the migration from memory, we have left it out. Upgrade advice assembled from recollection is the exact failure mode that makes this kind of article untrustworthy, and one unverified entry would put the other six in doubt.

Frequently Asked Questions

How were these bundle sizes measured?
Each version was installed on its own into an empty directory, then `export * from '<package>'` was bundled with esbuild in esm/browser/minified/production mode with React marked external, and the output gzipped at level 9. Both sides of every jump went through the identical pipeline, so the delta is comparable even where the absolute number is not what your app will pay.
Why does export * overstate what I would actually ship?
Because it imports the entire public surface and gives the bundler no chance to shake anything out. Import three icons from lucide-react and you pay a fraction of the 172 kB figure here. The ceiling is the number we can reproduce, which is why we publish it, and the delta between two ceilings is a fair comparison even when neither is your real cost.
Is a bigger bundle after a major upgrade a reason not to upgrade?
Rarely on its own. TanStack Table grew 114% across v9 and still ships less than 33 kB, which is noise next to most application bundles. Size matters when it is large in absolute terms, when it lands on a route that must be fast, or when the growth is unexplained by any feature you wanted.
Why is framer-motion missing from this list?
It crossed from 12 to 13 in the same window, but we could not retrieve release notes for that major from the repository, and we do not publish migration advice we have not verified against a primary source. The measurement exists (64.0 kB for 13.1.1); the migration notes do not, so the entry is not here.