docs/bundle-optimization-react-aria.md
Meteor's bundler (standard-minifier-js) does not perform tree-shaking. When any code imports from the react-aria barrel package:
import { FocusScope } from 'react-aria';
Meteor resolves the barrel's entry point, which re-exports all 43 sub-packages (@react-aria/dnd, @react-aria/calendar, @react-aria/table, etc.). Since there is no dead-code elimination, every sub-package ends up in the main JS bundle — even if only FocusScope is used.
The same applies to react-stately (20 sub-packages).
Measured impact before the fix: 832 KB of react-aria + react-stately in the main bundle, of which ~700 KB was unused code.
Two complementary changes:
yarn patch replaces the barrel entry points (dist/main.js, dist/module.js, dist/import.mjs) with slim versions that only re-export the sub-packages actually used.
The patches live in .yarn/patches/ and are referenced in package.json resolutions. They are applied automatically on every yarn install.
react-aria — retained sub-packages:
button, focus, i18n, interactions, listbox, menu, overlays, select, separator, slider, utils, visually-hidden, dialog
react-stately — retained sub-packages:
collections, menu, overlays, select, slider, tree
All imports across apps/meteor and workspace packages (gazzodown, ui-client, ui-contexts, ui-voip) were changed from barrel to direct sub-package imports:
// Before
import { FocusScope } from 'react-aria';
import { useOverlayTriggerState } from 'react-stately';
// After
import { FocusScope } from '@react-aria/focus';
import { useOverlayTriggerState } from '@react-stately/overlays';
This is the correct long-term fix, but alone it was not enough because @rocket.chat/fuselage (published npm package) still uses require("react-aria") internally via its UMD build. The yarn patch covers that case.
| Metric | Before | After | Delta |
|---|---|---|---|
| Main JS (minified) | 3688 KB | 3199 KB | -489 KB (-13%) |
| Main JS (gzip) | 939 KB | 841 KB | -98 KB (-10%) |
Zod v4 re-exports all 50 locale files from its main entry point:
// zod/v4/classic/external.js
export * as locales from "../locales/index.js";
This means import { z } from 'zod' pulls in error messages for Arabic, Hebrew, Thai, Russian, and 46 other languages — even though Rocket.Chat only uses the English locale (loaded by default via config(en())).
Measured impact: 147 KB (50 locale files) out of 278 KB total for zod — 53% of the zod bundle was unused locale data.
A yarn patch on [email protected] removes the export * as locales line from the barrel. The English locale (en.js) remains loaded since it's imported separately by config(en()). All other locales remain available for explicit import if needed:
// Still works:
import pt from 'zod/v4/locales/pt';
import { config } from 'zod';
config(pt());
When upgrading zod, the patch must be re-created:
package.json resolutions.yarn/patches/zod-*.patchyarn installyarn patch zod@npm:<new-version>export * as locales line from v4/classic/external.jsexports.locales require from v4/classic/external.cjsyarn patch-commit -s <patch-folder>If a feature requires localized zod error messages (e.g., form validation in the user's language), import the specific locale directly instead of relying on the barrel:
import de from 'zod/v4/locales/de';
import { config } from 'zod';
config(de());
This loads only the one locale needed (~3 KB) instead of all 50 (~147 KB).
If you need to use a react-aria hook or component that is not in the slim barrel (e.g., useCalendar, useTable, useDrag), you must:
Import from the sub-package directly (preferred):
import { useCalendar } from '@react-aria/calendar';
Or update the yarn patch if the import comes from a dependency you don't control (like @rocket.chat/fuselage):
yarn patch react-aria@npm:3.37.0
# Add the missing export to dist/import.mjs, dist/module.js, and dist/main.js
yarn patch-commit -s <patch-folder>
When upgrading these packages, the yarn patches must be re-created for the new version:
package.json resolutions.yarn/patches/react-aria-*.patch and react-stately-*.patch filesyarn install to get the unpatched versionyarn patch react-aria@npm:<new-version> and re-apply the slim barrelyarn patch react-stately@npm:<new-version> and re-apply the slim barrelWhen the fuselage package is updated, check if it uses any new react-aria exports. You can verify with:
grep -oE 'react_aria_1\.[a-zA-Z]+' node_modules/@rocket.chat/fuselage/dist/fuselage.development.js | sort -u
grep -oE 'react_stately_1\.[a-zA-Z]+' node_modules/@rocket.chat/fuselage/dist/fuselage.development.js | sort -u
If there are new exports, add them to the yarn patch.
The react-aria/react-stately yarn patches are a workaround. The proper fix is for @rocket.chat/fuselage to import from sub-packages directly instead of the barrel, which eliminates the need for those patches entirely. Once that is done, only the direct sub-package imports in source code are needed.
The zod patch is a workaround for a design choice in zod v4 (export * as locales in the main barrel). This may be addressed upstream — track https://github.com/colinhacks/zod for changes to the locale export strategy.
Meteor generates a .stats.json file alongside each build. To inspect it:
# Find the latest stats file
ls -t .meteor/local/build/programs/web.browser/*.stats.json | head -1
# Quick summary
python3 -c "
import json
with open('<stats-file>') as f:
data = json.load(f)
print(f'Total: {data[\"totalMinifiedBytes\"]/1024:.0f} KB')
print(f'Gzip: {data[\"totalMinifiedGzipBytes\"]/1024:.0f} KB')
"
The stats file contains a per-package breakdown in minifiedBytesByPackage, with packages/modules.js containing a nested tree of every npm module included in the main bundle.