Skip to content
All articles
PerformanceBundle Size

The Cost of JavaScript: A Deep Dive

Bundle analysis, tree-shaking, and how we cut Total Blocking Time by 60% at Mamaearth.

2 May 20269 min read

JavaScript Is the Most Expensive Resource on the Web

A 200KB image and 200KB of JavaScript are not equivalent. The image needs to be downloaded and decoded. The JavaScript needs to be downloaded, parsed, compiled, and executed — and during execution, it blocks the main thread, making the page unresponsive.

At Mamaearth, we had a Total Blocking Time of 1400ms on mobile. Our total JavaScript payload was 680KB compressed. By the time we were done optimizing, we'd cut the payload to 280KB and TBT to 520ms. This is the story of how we got there.

Step 1: Understanding What You Ship

The first step in reducing JavaScript cost is understanding what you're actually shipping. We used webpack-bundle-analyzer to generate a visual treemap of our bundles. The results were eye-opening.

Lodash accounted for 72KB despite us using only 4 functions. Moment.js with all locales was 67KB — we only needed English. A charting library that rendered one dashboard chart was 95KB. And our own application code? Only 180KB of the 680KB total.

The lesson: most JavaScript bloat comes from dependencies, not your code. Before optimizing your components, audit your node_modules.

Step 2: Tree-Shaking and Dead Code Elimination

Tree-shaking sounds magical — your bundler automatically removes unused code. In practice, it only works when dependencies use ES module exports and don't have side effects. Many popular libraries fail one or both of these conditions.

We replaced Lodash with lodash-es and switched our imports from import _ from 'lodash' to import { debounce } from 'lodash-es'. This single change saved 65KB. We replaced Moment.js with date-fns, which is tree-shakeable by design, saving another 50KB.

For libraries that couldn't be tree-shaken, we used dynamic imports to load them only when needed. The charting library that rendered one dashboard chart was moved behind a dynamic import that loaded only when the user navigated to the dashboard. This removed 95KB from the initial bundle.

Step 3: Code Splitting by Route

Next.js gives you automatic code splitting by page, but shared modules can negate this. If three pages import a component that imports a heavy library, that library ends up in the shared chunk and loads on every page.

We audited our shared chunks and found that several large components were shared across only two or three pages but were loaded on all pages because they were imported through a barrel export. Moving these imports to direct paths reduced the shared chunk by 40%.

We also implemented component-level code splitting for heavy interactive elements like image carousels, rich text editors, and map embeds. These components are loaded when they scroll into view using Intersection Observer combined with React.lazy. The user never downloads code for components they don't interact with.

Step 4: Compression and Delivery

After reducing the raw JavaScript size, we optimized how it was delivered. We switched from gzip to Brotli compression, which achieved 15-20% better compression ratios for JavaScript. We served Brotli-compressed static assets from a CDN with long cache headers.

We also implemented resource hints — preloading critical chunks and prefetching likely next-page chunks. When a user is on the product listing page, we prefetch the product detail page bundle. By the time they click, the JavaScript is already in the browser cache.

The combined effect of all four steps: JavaScript payload dropped from 680KB to 280KB, TBT dropped from 1400ms to 520ms, and Time to Interactive improved by 2.1 seconds on 4G mobile. The conversion rate on mobile increased by 7% in the month following the optimization.

Maintaining the Gains

Optimization without maintenance is temporary. New features add new JavaScript, and without guardrails the bundle will grow back to its original size within a few months. We implemented the performance budgets I described in my earlier article — CI checks that fail if any route exceeds its JavaScript budget.

We also added a quarterly bundle audit to our team rituals. Once a quarter, we regenerate the bundle analysis, compare it against the previous quarter, and identify any new bloat. This ritual takes an hour and consistently finds 20-30KB of unnecessary JavaScript that crept in during feature development.

Found this useful? I write about engineering, performance, and career growth.