Skip to main content

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.

ItemSize
common.js (single chunk)18.97 MB (about 5.1 MB gzipped)
Images, fonts and other non-JS files11.6 MB
Total in dist/apps/productionabout 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 and sourceMap: true). The real dist/ was not touched.
  • Attributed every minified byte of common.js to its source file using the source map, then grouped by package.
  • Counted import sites per dependency across libs/ and apps/.
  • Sizes below are minified bytes. Savings figures are estimates and have not been applied or tested.

Where the JS bytes go​

PackageSizeUsed by
plotly.js4.7 MB3 files (GWI and ILM 3D plots)
@babel/standalone and its helper packagesabout 3 MB or more1 file (AI Dashboard live compiler)
MUI core, x-data-grid, x-date-pickersabout 1.5 MBused everywhere
antd0.8 MB1 file (AI Dashboard)
maplibre-gl and its style specabout 1 MB5 files
refractor (code highlighting)0.6 MB1 file (chat bubble)
moment0.38 MB, plus 0.31 MB of locale files128 files
recharts0.34 MB39 files
lottie-web0.31 MB4 small JSON animations
mixpanel-browser0.31 MBanalytics
xlsx0.28 MB1 file (export)
Your own code, all libs combinedabout 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)​

FileSizeFix
landing_page_demo.gif2.6 MBConvert to mp4 or webm (usually over 90% smaller)
radhakunj_map.png2.3 MB (1156×1166)Convert to WebP or compress
madiwala_lake.png2.3 MBConvert to WebP or compress
offline_indication.svg540 KBIt is a 26×26 icon with an embedded base64 raster. Re-export as a real SVG or a tiny PNG
fluxgen_logo.png368 KB (4096 px wide)Resize. A smaller full_logo.png already exists
water_bg.png, test_report.png, radhakunj_graph.pngabout 2 MB combinedCompress

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) to PrismLight with a few registered languages. Saves about 0.6 MB.
  • Load xlsx with await import('xlsx') inside libs/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-object belong in devDependencies or can go.
  • libs/waterRisk/src/components/RenderLake.jsx imports @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.