A bundle report for a production build shows the same package — say date-fns — appearing twice under two different node_modules paths at two different versions. Why does the bundler emit both copies, and how do you get down to one?
answer
- two ranges, two installed copies
- bundler resolves paths, not names
- ask the package manager who wants it
- instanceof and singletons break too
- overrides force it, tests must cover it
basics
~20 sTwo dependents asked for incompatible version ranges, so the package manager installed a nested second copy. The bundler resolves each import to its own file on disk, so both are distinct modules and both ship. You fix it by aligning the ranges, not in the bundler.
solid answer
~50 sThe duplicate is an install-tree fact, not a bundler bug. When two dependents declare ranges that cannot be satisfied by one version, npm, Yarn and pnpm all place a second copy nested under the dependent that needs it. To a bundler those are two different files at two different paths, so it includes both, and you pay for the library twice. Diagnose it by asking the package manager who pulled each copy in — `npm ls date-fns`, `yarn why date-fns` or `pnpm why date-fns` prints the dependent chain. The real fix is upstream: upgrade the dependent that pins the old range, or align your own ranges so a single version satisfies everyone. When you cannot, force resolution with npm's `overrides` (npm 8.3+), Yarn's `resolutions`, or pnpm's `overrides` — and then actually test, because you have just given a package a version it never declared support for. Libraries that must be singletons ship as peer dependencies precisely to make this visible.
code
bash · 3 linesnpm ls date-fns
npm dedupe
npm ls date-fnsgo deeper
Know that the same package can be installed at two versions and that both then ship. Be able to run the package manager's dependency query to see who asked for each one.
Explain that resolution happens by file path, so nested copies are separate modules, and walk through the fix order: align ranges, deduplicate, force a version only as a last resort.
Bring up the failure modes beyond size — broken instanceof checks, duplicated singletons and registries — and treat a forced override as a change that needs test coverage and a comment recording why.
Own the policy angle: peer dependencies for anything that must be a singleton, a lockfile people actually review, and a standing check for repeated packages so duplication is caught at install time rather than in a bundle report months later.
## What a duplicate in the report means A bundler does not resolve imports by package name; it resolves them to **absolute file paths**. If one import lands on `node_modules/date-fns/format.js` and another on `node_modules/some-lib/node_modules/date-fns/format.js`, those are two unrelated modules as far as the build is concerned, and both are included. The treemap is simply reporting the truth of the module graph. The cause is the install tree. Package managers must satisfy every declared range. When your app depends on `date-fns@^3` and a chart library depends on `date-fns@^2`, no single installed version satisfies both, so the manager hoists one to the top level and nests the other under the dependent that needs it. Nothing is broken — the app works — you are just shipping the library twice. ```bash # Who pulled in each copy? npm ls date-fns # prints every dependent and the version it resolved to # or, depending on the manager yarn why date-fns pnpm why date-fns ``` ## Why it costs more than bytes The obvious cost is payload: two copies of a 40 kB library is 40 kB of pure waste, and it comes with matching parse and compile time. There are two subtler costs. First, **module identity breaks**. Anything that relies on a single shared instance across the app stops working when there are two: a class whose instances are checked with `instanceof`, a plugin registry, a module-level singleton cache, a context or store object. Code that looks correct fails with confusing symptoms because object A came from copy 1 and the check lives in copy 2. Some libraries guard against this by warning at runtime when they detect multiple instances. Second, **the duplicate hides**. It rarely appears as a bug report. It shows up as a bundle that is quietly larger than anyone expects, which is exactly why looking for repeated package names is a standard step when you read a bundle report. ## Working the fix, in order of preference **1. Align the ranges honestly.** Look at the dependency chain the manager printed. Usually one dependent is behind. Upgrading it so its range overlaps yours collapses both copies into one, and it is the only fix that leaves the tree truthful. Sometimes the reverse is true — you are on a new major that nothing else supports yet, and moving your own range down is the cheaper move. **2. Let the manager deduplicate.** If the ranges *do* overlap but the tree was built badly across incremental installs, `npm dedupe` (or reinstalling from a clean lockfile) can flatten it. This only works when a single version genuinely satisfies everyone; it will not resolve a real conflict. **3. Force a single version.** Every manager has an escape hatch: npm's `overrides` field (available from npm 8.3), Yarn's `resolutions`, pnpm's `overrides`. These rewrite what a dependent gets regardless of what it asked for. ```json { "overrides": { "date-fns": "^3.0.0" } } ``` This is a real intervention, not a formality. You have told a package to run against a version it never declared support for. If the majors differ, the API may have changed underneath it. Use it when the version gap is small or the surface used is narrow, pin the override explicitly, and make sure the affected paths are covered by tests. Leave a comment saying why the override exists — the next person will otherwise delete it. ## The singleton case For libraries where two copies are semantically wrong rather than merely wasteful, the ecosystem convention is a **peer dependency**: the library declares that its host must provide the package, rather than depending on it directly. That converts a silent duplication into an install-time warning or error, which is the point. When you publish a package that plugs into a framework or a state library, declaring it as a peer dependency is what prevents your consumers from finding two copies in their bundle report. ## Not every duplicate is a mistake Before you spend a day on it, check the weight and the chunk. Two copies of a 3 kB utility inside a rarely loaded chunk is not worth a risky override. And be careful reading paths: monorepo workspaces and pnpm's symlinked store can make one physical copy appear under several paths in a report. Confirm with the manager's own dependency query before concluding you have two.
- How would a library author reduce the chance of consumers ending up with two copies?Declare the shared package as a peer dependency rather than a direct dependency, so the host app supplies the single instance and the package manager warns when it cannot be satisfied. Keeping the accepted range wide, and avoiding hard pins on transitive libraries, also removes most conflicts before they reach a consumer's bundle.
- Your report shows the same package under several paths, but the package manager reports one version. What is happening?Almost certainly one physical copy surfacing through several paths — pnpm's symlinked store and monorepo workspace links both do this, and some analyzers show the link path rather than the real one. Trust the manager's dependency query over the visual grouping, and confirm by checking whether the two entries have identical sizes and contents.
saying these in an interview costs you the question
- Blames the bundler for including both copies
- Thinks deleting node_modules fixes a real range conflict
- Applies an override without testing the forced dependent
- Says duplicates only waste bytes, never break behaviour
- Assumes every repeated path in a report is a real duplicate