A 32 MB JavaScript chunk: how a few icons broke my dev server
My dev server kept throwing ChunkLoadError timeouts. The root layout chunk had grown to 32 MB, and almost all of it came from importing a handful of icons.
The problem
Random ChunkLoadError: Loading chunk app/layout failed (timeout) in development.
The result
Layout chunk went from 32 MB to 3.6 MB; the timeouts stopped.
Stack
The problem
From time to time, my local site crashed with this error:
Unhandled Runtime Error
ChunkLoadError: Loading chunk app/layout failed.
(timeout: http://localhost:3000/_next/static/chunks/app/layout.js)
app/layout is the root of every page, so when it fails, nothing renders.
The details
A timeout on a local file is suspicious. Localhost should be instant. So the first thing I checked was the size of the chunk itself:
| File | Size |
|---|---|
app/layout.js | 32 MB |
Thirty-two megabytes for a layout with a header and a footer. Next, I measured which modules made up that file:
| Module | Size in chunk |
|---|---|
react-icons/si | 14.4 MB |
react-icons/fa6 | 5.5 MB |
react-icons/fa | 4.4 MB |
react-icons/io5 | 3.5 MB |
react-icons/ci | 1.1 MB |
About 29 MB was icons, and I only used one or two icons from each pack: LinkedIn, GitHub, WhatsApp, Gmail, Instagram and X in the footer, plus a menu icon in the header.
The reason: each react-icons pack is one big file containing every icon. In production, tree-shaking removes the unused ones. In development, there is no tree-shaking, so importing one icon pulls in the entire pack. Five packs, 29 MB.
The challenges
- Production was fine. The bug only existed in development, where the bundler skips tree-shaking. Measuring the production bundle would have shown nothing wrong.
- The error message pointed at the network, not at the bundle size.
- Barrel optimization didn't help. Next.js can optimize "barrel" imports, but a
react-iconspack isn't a barrel of small files: it's a single file, so there is nothing to split. - I didn't want visual changes. The icons had to look and behave exactly the same, including the
sizeandclassNameprops.
The solution
I generated a small local Icons.tsx containing only the icons the site uses, copying their SVG data directly from the installed package, and kept using the library's tiny GenIcon helper:
import { GenIcon, IconBaseProps, IconTree } from "react-icons";
const FaGithubData: IconTree = { tag: "svg", attr: { viewBox: "0 0 496 512" }, child: [/* ... */] };
export const FaGithub = (props: IconBaseProps) => GenIcon(FaGithubData)(props);
Then every import changed from react-icons/fa6 to the local file. 11 icons, 11 KB instead of 29 MB.
| Before | After | |
|---|---|---|
app/layout.js (dev) | 32 MB | 3.6 MB |
Lessons learned
- Measure before guessing. "Timeout" sounded like a network issue; the real issue was a 32 MB file.
- Dev and production bundles behave differently. Tree-shaking hides problems in production that still hurt your daily development.
- Look at what a library ships, not just what you import. One named import can mean megabytes.
- Fix the cause, not the timeout. Raising the chunk timeout would have hidden the problem and kept the slow dev server.
Facing a similar problem?
I help teams fix performance, backend and reliability issues in production.
Let's talk