Skip to content

fix(build): preserve ESM modules so bundlers can tree-shake unused Firebase SDKs - #838

Open
jhuleatt wants to merge 2 commits into
mainfrom
fix/esm-tree-shaking
Open

jhuleatt wants to merge 2 commits into
mainfrom
fix/esm-tree-shaking

Conversation

@jhuleatt

@jhuleatt jhuleatt commented Oct 27, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

Previously, vite.config.ts bundled all of src/* into a single monolithic dist/index.js that imported every Firebase product (firebase/firestore, firebase/auth, firebase/database, firebase/storage, firebase/functions, firebase/remote-config, firebase/performance) at the top of one file. Because all product imports lived inside a single pre-bundled chunk, consumer bundlers could not tree-shake unused Firebase SDKs or split them behind React.lazy.

This PR:

  1. Emits one ES module per source file (preserveModules: true, preserveModulesRoot: 'src', entryFileNames: '[name].js') alongside dist/index.umd.cjs.
  2. Externalizes rxjs, rxjs/operators, and rxfire/* (declared in dependencies) so consumer bundlers resolve and tree-shake them directly instead of inlining a duplicate copy.
  3. Updates "rxjs" in dependencies from "^6.6.3 || ^7.0.1" to "^7.1.0" so external resolution always provides RxJS ≥ 7.1 (required by share({ resetOnRefCountZero: () => timer(...) })).
  4. Adds "sideEffects": false to package.json.
  5. Sets "target": "es2024" in tsconfig.json (Baseline-certified) so TypeScript does not inject virtual _tslib.js helper chunks under preserveModules: true.

Bundle Size Impact

Measured with Vite 8.3.5 production builds against firebase@13.1.0 (gzip -9 of the emitted application bundle):

Consumer App Raw Firebase Baseline (No ReactFire) reactfire@4.2.6 (main) With This PR (preserveModules: true) Savings vs. 4.2.6
Auth-only (useUser) 96.0 KB (95,988 B) 269.1 KB (269,074 B) — bundles Firestore, RTDB, Storage, Perf, Functions, RC 103.8 KB (103,791 B) — only firebase/auth + rxjs + rxfire/auth -165.3 KB gzip (-61.4%)
Firestore-only (useFirestoreCollection) 206.8 KB (206,792 B) 279.0 KB (279,049 B) — bundles Auth, RTDB, Storage, Perf, Functions, RC 214.5 KB (214,541 B) — only firebase/firestore + rxjs + rxfire/firestore -64.5 KB gzip (-23.1%)

Backwards Compatibility & Release Notes

  • Identical Public Exports (74 / 74): Both import * as RF from 'reactfire' (dist/index.js) and require('reactfire') (dist/index.umd.cjs) expose the exact same 74 named exports as v4.2.6.
  • Unchanged Entrypoints: package.json "main" (dist/index.umd.cjs), "exports" ("." -> { "import": "./dist/index.js", "require": "./dist/index.umd.cjs" }), and "typings" (dist/index.d.ts) are unchanged.
  • Safe "sideEffects": false: All globalThis caches (_reactFirePreloadedObservables, _reactFireFirestoreQueryCache, _reactFireDatabaseCachedQueries) are initialized inside the modules that read them (useObservable.ts, firestore.tsx, database.tsx), so dropping an unused product module (e.g., dist/database.js or dist/performance.js) never drops state needed by used modules.
  • Release Note (rxjs ^7.1.0): Because rxjs is a regular dependency (and v4.2.6 already bundled rxjs@7.8.1), runtime behavior and npm install are unchanged. However, TypeScript applications still on rxjs@6 that pass their own custom Observable instances directly into useObservable or preloadObservable will need to upgrade to rxjs@^7.1.0 so TypeScript's Observable types match.

Closes #489
Closes #488
Refs #480

@jhuleatt
jhuleatt requested a review from tyler-reitz October 27, 2026 18:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

@firebase/database included in the final build while I only use firestore Reactfire preventing lazy loading of firebase modules

1 participant