WhollySoftware
Back to blogWeb

Reducing JavaScript Bundle Size Without Breaking Features

Wholly Software TeamMay 26, 20266 min read
Reducing JavaScript Bundle Size Without Breaking Features

A client's dashboard app had a main JavaScript bundle of 1.4MB gzipped, and the instinct on projects like this is usually to blame the framework or start planning a rewrite. Running the bundle through webpack-bundle-analyzer told a different story: a chart library that supported a dozen chart types was fully included even though the app only used two of them, a moment.js dependency was pulled in by a single date-formatting utility despite the rest of the app already using date-fns, and a UI library was imported wholesale instead of per-component.

The chart library swap alone — moving to a lighter library with proper tree-shaking support for the two chart types actually in use — cut about 220KB gzipped. Removing the duplicate date library, replacing the one moment.js usage with the date-fns equivalent already present elsewhere in the codebase, removed another 60KB that had been pure duplication nobody had noticed accumulating.

Route-based code splitting was underused — the entire app, including admin-only screens that maybe 2% of users ever see, was bundled into the initial load. We split the app by route using dynamic imports, so a regular user's first load only includes the code for the dashboard home and common navigation, with admin screens, the reporting module, and a rarely-used bulk-import tool each loading on demand when actually navigated to.

Tree-shaking gaps in a few third-party packages needed manual intervention. One utility library wasn't marked with sideEffects: false in its package.json, which meant webpack couldn't safely eliminate unused exports and was including the entire library despite the app only using three functions from it. We worked around it with a targeted import path change pointing directly at the specific submodules rather than the library's barrel export, which let the bundler drop the rest.

The end result was 480KB gzipped for the main bundle, a 66% reduction, with zero features removed and no user-facing behavior change — every byte cut was either genuinely unused code, an accidental duplicate, or code that didn't need to load until a specific route was visited. Time-to-interactive on a mid-tier Android device dropped from 5.8s to 2.6s, which mattered more to this client's actual user base than any desktop benchmark would have shown.

Web PerformanceJavaScriptBundlingCore Web Vitals