Production build size analysis
Date: 2026-10-29
Scope: apps/production (Nx + Rspack), branch feature/pk_ai_dashboard
Summary
The app ships as one 19 MB JavaScript file, and nothing is loaded on demand. Every page, chart library and image is downloaded before the login screen shows.
| Item | Size |
|---|---|
common.js (single chunk) | 18.97 MB (about 5.1 MB gzipped) |
| Images, fonts and other non-JS files | 11.6 MB |
Total in dist/apps/production | about 30 MB |
The other apps have the same problem and are larger: demo 34 MB of JS, lakepulse 30 MB, uwms 19 MB, rwi 18 MB, mobile 16 MB. None of their route files use lazy() either.
Method
- Built the production target into a scratch folder with source maps enabled (
nx build production --skip-nx-cache, with an overridden output path andsourceMap: true). The realdist/was not touched. - Attributed every minified byte of
common.jsto its source file using the source map, then grouped by package. - Counted import sites per dependency across
libs/andapps/. - Sizes below are minified bytes. Savings figures are estimates and have not been applied or tested.
Where the JS bytes go
| Package | Size | Used by |
|---|---|---|
| plotly.js | 4.7 MB | 3 files (GWI and ILM 3D plots) |
| @babel/standalone and its helper packages | about 3 MB or more | 1 file (AI Dashboard live compiler) |
| MUI core, x-data-grid, x-date-pickers | about 1.5 MB | used everywhere |
| antd | 0.8 MB | 1 file (AI Dashboard) |
| maplibre-gl and its style spec | about 1 MB | 5 files |
| refractor (code highlighting) | 0.6 MB | 1 file (chat bubble) |
| moment | 0.38 MB, plus 0.31 MB of locale files | 128 files |
| recharts | 0.34 MB | 39 files |
| lottie-web | 0.31 MB | 4 small JSON animations |
| mixpanel-browser | 0.31 MB | analytics |
| xlsx | 0.28 MB | 1 file (export) |
| Your own code, all libs combined | about 1.6 MB |
Plotly and Babel are roughly 40% of the JS and serve three screens. Your own code is under 10%.
Recommendations (biggest wins first)
1. Lazy-load every route
apps/production/src/routes/routes.js has about 60 static page imports and no lazy() calls. Switch to React.lazy or the router's lazy: option, with a <Suspense> fallback in MainLayout. This makes every fix below apply only to the page that needs it.
Estimate: first load drops from 19 MB to about 3–5 MB raw (about 1 MB gzipped).
2. Trim plotly.js
react-plotly.js pulls in the full 4.7 MB build. The code only uses these trace types: surface, mesh3d, scatter3d, scatter, heatmap.
Files: libs/gwi/src/components/MapDisplayComponent.jsx, libs/gwi/src/components/RejuvenationImpactView.jsx, libs/ilm/src/components/Hydrography3D.jsx.
Use react-plotly.js/factory with plotly.js/lib/core plus those trace modules (about 1.5–2 MB), and lazy-load the three screens. Estimated saving: about 3 MB.
3. Isolate the Babel and antd compiler
libs/aiDashboard/src/components/DynamicComponentLoader.jsx imports @babel/standalone (3.15 MB minified) and import * as AntD from 'antd', which blocks tree-shaking.
- Load the component with
import()only when the AI Dashboard opens. - Longer term, compile the AI-generated JSX on the backend or with a much smaller compiler such as sucrase.
Estimated saving: about 4 MB from the first load.
4. Fix the images (11.6 MB of non-JS output)
| File | Size | Fix |
|---|---|---|
landing_page_demo.gif | 2.6 MB | Convert to mp4 or webm (usually over 90% smaller) |
radhakunj_map.png | 2.3 MB (1156×1166) | Convert to WebP or compress |
madiwala_lake.png | 2.3 MB | Convert to WebP or compress |
offline_indication.svg | 540 KB | It is a 26×26 icon with an embedded base64 raster. Re-export as a real SVG or a tiny PNG |
fluxgen_logo.png | 368 KB (4096 px wide) | Resize. A smaller full_logo.png already exists |
water_bg.png, test_report.png, radhakunj_graph.png | about 2 MB combined | Compress |
libs/shared/src/assets/assets.js imports every image, so all of them are always built and deployed. Import the large ones only in the components that use them, or serve them from a CDN or blob storage.
5. Quick wins
- Ignore moment locales with
IgnorePlugin(moment/locale). Saves about 310 KB, and no file imports a locale. - Switch the chat bubble (
libs/aquaAi/src/components/AssistantBubble.jsx) toPrismLightwith a few registered languages. Saves about 0.6 MB. - Load
xlsxwithawait import('xlsx')insidelibs/shared/src/utils/downloadUtil.js. Saves 0.28 MB. - Import only
@fontsource/roboto/latin-*.css. Browsers already fetch only the subsets they need, so this just removes about 0.7 MB of unused font files from the deploy (72 font files today).
6. Delay non-critical startup work
Mixpanel (0.3 MB), the device fingerprint library (0.12 MB) and lottie-web (0.3 MB) are loaded up front. Load them after first render, or inside the components that use them.
7. Clean up dependencies
No source file imports these package.json entries: loadash (typo), react-heatmap-grid, @sentry/react, @chatscope/chat-ui-kit-react, flytask-joyride, react-responsive, prop-types, web-vitals. They add nothing to the bundle but slow installs.
Also:
@testing-library/*and@babel/plugin-proposal-private-property-in-objectbelong indevDependenciesor can go.libs/waterRisk/src/components/RenderLake.jsximports@react-three/*, which is not installed, and nothing imports the lib. It looks dead.
8. Check compression and caching on Azure
apps/production/src/web.config only has the rewrite rule: no <urlCompression> and no caching settings. Make sure IIS compresses JS, CSS and SVG, and sets long-cache headers on the hashed files. Firebase Hosting compresses automatically.
9. Longer term
- Replace moment with dayjs or date-fns (128 files, so a bigger migration).
- Add a bundle analyzer step to CI so a size jump is visible before it ships.
Not verified
- Why the demo and lakepulse bundles are 30–34 MB.
- The Capacitor mobile build, where the JS ships inside the app.
- Whether Azure currently compresses responses.
- The estimated savings above. They need to be measured after each change.