ZyVOP Logo
Content That Connects
SeriesAI NewsLeaderboardWrite for Us
ZyVOP Logo
Content That Connects

Empowering developers and creators with cutting-edge insights, comprehensive tutorials, and innovative solutions for the digital future.

Content

  • Categories
  • Tags
  • Badges
  • Leaderboard
  • Write Article
  • Newsletter

Company

  • About Us
  • API Documentation
  • Write for Us
  • Contact

Connect

  • Privacy Policy
  • Terms of Service
  • Cookie Policy
  • DMCA Policy
  • Code of Conduct

© 2026 ZyVOP. Crafted with care for the developer community.

Made with ❤️ by the ZyVOP team
All systems operational
HomeCode-Splitting Is a Boundary Decision, Not a Bundle Trick

Code-Splitting Is a Boundary Decision, Not a Bundle Trick

Maksym Kuzmitskyi (MaximusFT)
Maksym Kuzmitskyi (MaximusFT)Staff Frontend Engineer
August 7, 2026
6 min read
Code-Splitting Is a Boundary Decision, Not a Bundle Trick
#react#React Playbook#Architecture#performance

Code-Splitting Is a Boundary Decision, Not a Bundle Trick

The command palette article closed by promising the last piece of this look-and-feel stretch, and it's the one that looks the most like a solved problem: code-splitting. Wrap a component in React.lazy, drop a Suspense boundary around it, watch the bundle shrink. Done, right?

Here's the thing that bugs me about most bundle-size advice. It's all about the how — lazy, dynamic import(), the router's lazy routes — and almost none of it is about the where. And the where is the entire problem. Splitting code is one line. Deciding what to cut, so the app loads fast without shattering into a hundred chunks you can't reason about, is an architecture decision. Get the how right and the where wrong, and you don't get a faster app. You get a slower one with more moving parts.

A split is not free

Start with what a split actually costs, because the mental model everyone skips is that lazy-loading isn't "free performance." Every split you introduce is a boundary, and every boundary buys you three things you didn't have before: a separate network request, a loading state you now have to design, and the risk of a waterfall if that request can't start until another one finishes.

A code-split isn't a size optimization. It's a boundary you're adding to the app — and boundaries have a cost on both sides.

So the real question stops being "can I split this?" (you almost always can) and becomes "is there a real seam here worth paying a boundary for?" That reframe is the whole article.

The default that quietly backfires

The advice you'll read first is "split at the route level," and honestly, that advice is correct. Route boundaries are the best seams in the entire app: the user is already waiting for a navigation, they don't expect the next screen to be instant, and each route pulls in a genuinely different slice of the app. With a type-safe router like the one from the routing article, lazy routes are the natural unit:

// each route is its own chunk — the user is already navigating, a boundary here is invisible
export const Route = createFileRoute('/reports')({
  component: lazyRouteComponent(() => import('./ReportsPage')),
});

Where it backfires is the next step people take. Route-level splitting works, so they conclude "more splitting is more better" and start wrapping individual components — a UserAvatar here, a Badge there — in lazy. Now every one of those is a network round-trip and a spinner. You've turned a single fast download into a cascade of tiny requests, each with its own loading flicker, and the page assembles itself in front of the user like a slideshow. That's not a faster app. That's the same bytes, delivered worse.

Where the seams actually are

So if "everywhere" is wrong and "routes" is the safe default, what else genuinely earns a split? Three shapes, and they all have the same property: the code is heavy and not needed for the first meaningful paint.

Heavy, rarely-used features. A rich text editor, a video player, a PDF viewer, the chart library you only render on the analytics tab. These can each be hundreds of kilobytes, and most sessions never touch them. Splitting here is pure win — the cost of the boundary is paid only by the users who actually open the feature.

// the editor is 300kb of the bundle and only the 5% who click "edit" ever need it
const RichTextEditor = lazy(() => import('./RichTextEditor'));

function Note({ isEditing }: { isEditing: boolean }) {
  if (!isEditing) return <NotePreview />;
  return (
    <Suspense fallback={<EditorSkeleton />}>
      <RichTextEditor />
    </Suspense>
  );
}

Behind an interaction. Anything that only appears after a click — a settings modal, a command palette, an export dialog. The boundary hides behind the click the user already made, so the tiny load feels like part of the action, not a delay.

Below the fold, and truly optional. The stuff a user might scroll to but often won't.

Notice what's not on this list: anything needed for the first render, anything small, and anything that's split purely because it lives in its own folder. A 4kb component doesn't deserve a network request. The download of that request costs more than the bytes you saved.

The waterfall you'll create by accident

The nastiest failure mode isn't too many chunks — it's chunks that can't load in parallel. This is the same waterfall problem the data-fetching article warned about, just moved from data to code. A lazy component that, once loaded, immediately lazy-loads its own child, which lazy-loads its child — the browser can't start request two until request one lands. You've serialized what should have been parallel.

The fix is intent-based prefetching: start fetching the chunk when the user signals they're heading somewhere, not when they arrive. Hovering a link, focusing a button, opening the parent — all of these are the browser's chance to fetch ahead:

// kick off the chunk on hover, so by the time they click it's already in cache
<Link
  to="/reports"
  onMouseEnter={() => import('./ReportsPage')}
>
  Reports
</Link>

Do this and the boundary all but disappears: the request overlaps with the user's own reaction time instead of stacking behind another request. Good routers do a version of this for you. The point is that when a chunk starts loading matters as much as whether you split it.

The chunk you can't reason about

Here's where splitting collides with the rest of the Playbook. Remember the barrel-file article — the index.ts that re-exports everything in a folder? Lazy-load a single component through a barrel, and you don't get that component. You get the whole barrel: every sibling it re-exports, dragged into the chunk because the import graph can't tell them apart. Your carefully-placed lazy boundary quietly pulls in half the folder, and the chunk you thought was small isn't.

// looks like one small component...
const Chart = lazy(() => import('./widgets').then((m) => ({ default: m.Chart })));
// ...but ./widgets/index.ts re-exports Chart, Table, Map, Calendar — all now in this chunk

This is why I keep coming back to the idea that these topics aren't separate. A split is only as clean as the module boundaries underneath it. If your imports reach sideways through barrels and shared grab-bags, your chunks will be blurry no matter how many lazy calls you sprinkle in. Import the file directly, and the split is exactly as big as you think it is.

Split where it hurts, not where the folders are

The re-renders article made an argument I want to borrow directly: optimize where it actually hurts, and don't let the optimization wreck the structure. Code-splitting is the same discipline one layer down. The temptation is to split on the shape of your file tree — one chunk per feature folder, because it's tidy. But the file tree is your convenience, not the user's journey. The seams that matter are the ones in their experience: the route they navigate to, the heavy tool they occasionally open, the dialog behind a click.

Before adding a lazy, I ask two questions. Is this code actually heavy enough that its bytes show up in the load? And is there a real moment in the user's flow where paying for a separate request is invisible? If both answers aren't yes, the split is making the app more complex for a number that doesn't move.

The reframe

Code-splitting reads like a bundle trick and behaves like an architecture decision — which makes it a fitting end to this look-and-feel cluster, because so did theming, and so did the command palette. Each one looks like polish and turns out to be a boundary in disguise. The visible artifact is trivial: a smaller bundle, a color toggle, a search modal. The value, every time, is in where you draw the line and whether the rest of the structure lets that line stay clean.

So the honest heuristic isn't "split more." It's split rarely and deliberately, at the seams the user can feel, over module boundaries clean enough that a chunk means what its name says. Everything else is just trading one problem you can see — a big bundle — for a dozen you can't.

If you've got a build with more than a handful of chunks, pull up your bundle analyzer and find the smallest lazy chunk you have. If it's a few kilobytes, that's a boundary you're paying for and getting nothing back — tell me what it is, because I'd bet it snuck in on the "more splitting is better" reflex, and it's the first thing I'd delete.

Maksym Kuzmitskyi (MaximusFT)

Maksym Kuzmitskyi (MaximusFT)

Staff Frontend Engineer

Passionate developer sharing knowledge about modern web technologies and best practices.

Comments (0)

Login to post a comment.

Related Posts

The Command Palette Is an Architecture, Not a Widget

The theming article ended on a move from how an app looks to how power users drive it. The command palette — the Cmd+K menu that fuzzy-searches everything you c...

Read article

You Probably Don't Need Multi-Agents

I work across six or seven repositories on one project — a big hybrid thing, part microfrontend, part backend, several apps that all talk to each other. When I ...

Read article

🚀 I Built a Full Stack Miro Clone with Real-Time Collaboration using Next.js

🚀 I Built a Full Stack Miro Clone with Real-Time Collaboration using Next.js After weeks of building, debugging, redesigning, and optimizing — I finally comple...

Read article

Misusing React Context, Then Blaming React Context

Stop blaming React Context for unnecessary re-renders. Discover how React composition affects rendering performance and how to build efficient Context providers.

Read article

From Zero-Latency Algorithms to Production Scale: Architectural Lessons Building High-Performance SaaS Ecosystems

A deep architectural exploration into scalable software engineering, zero-latency computational math, self-healing iframe widgets, and full-stack UI design systems from the founder behind ScaleQo and Quranbookk.

Read article