DEV Community

Cover image for I Joined My First Job and the Homepage Took Forever to Load
DeadLocker
DeadLocker

Posted on

I Joined My First Job and the Homepage Took Forever to Load

Thinking back to my first real frontend job still stings.

I wasn't a total coding newbie—I had a CS degree and plenty of full-stack school projects. But professional work? Zero experience. No production traffic, and no QA breathing down my neck about real users bouncing.

When I joined, the e-commerce homepage felt brutally slow. QA kept dropping the same messages week after week:

"Homepage lagging again 😭"

"Hero + product grid take forever."

"Users are leaving because of the 3s+ delay."

I'd open the reports, see those agonizing waterfalls, and just feel paralyzed.

The stack looked modern on paper: S3 + CloudFront + Single Page App (SPA). To make it "modular," we used iframes for sections like recommendations or banners. In theory, it was plug-and-play. In practice, it was a heavy, fragile mess.

I could sense something was off, but I didn't have the vocabulary yet to explain why: every iframe was exposing a massive, redundant dependency graph that our monolith had hidden.


The seductive simplicity of the monolith

iframe embedded every e-commerce pages

A single SPA feels clean when you first build it. One repo, one deployment. If you need to reuse a feature elsewhere, an iframe seems like a pragmatic shortcut: wrap the app, drop it in, ship fast.

That's the trap. An iframe doesn't just embed a UI; it embeds a runtime. We weren't just dropping a "banner" into the page; we were dropping an entire massive React ecosystem into a tiny window. Every "modular" section was forcing the browser to boot up a whole new instance of our giant store app.

The system was technically modern and architecturally overloaded at the same time. The iframes didn't create this overload—they made it impossible to ignore.

1. The weight problem: too much code for too little UI

The iframe trap

This is usually where the pain shows up first.

A user opens a simple promotion page, but the browser ends up downloading the entire store application: product listing logic, product detail logic, event handling, routing, shared components — and whatever else had quietly accreted over time.

That means you pay the cost of the whole mall just to visit one kiosk.

A monolithic SPA often creates this problem because its bundle grows with the number of features, not with the size of the page the user is actually viewing. In practice, that leads to:

  • larger JavaScript payloads
  • longer parse and execution time
  • slower first interaction
  • more memory pressure on weaker devices

The iframe made the mismatch visible. Instead of loading a tiny, purpose-built promotion surface, the browser had to initialize a full app inside an embedded context. The user sees one feature, but the machine does full-system work. We had one big system masquerading as several smaller ones.

Core performance lie: the UI looks small, but the runtime cost is large—and the iframe architecture exposed this lie by forcing us to pay that cost multiple times on one page.

2. The invisible content problem: what crawlers can't easily see

Crawler hidden content

The second failure mode is less visible, which is why teams often ignore it for too long.

The crawlability problem started with our client-rendered SPA architecture.
A client-rendered SPA already makes indexing fragile — search engines can execute JavaScript, but relying on that for your core product surface is a bet you probably shouldn't make. The iframe compounded this by adding another layer of indirection on top of an already shaky foundation.

That's the distinction worth making: the SPA made content invisible by default. The iframe made it harder to fix by scattering content across multiple embedded contexts.
If you strip out the iframe but keep a fully client-rendered product page, you still have a crawlability problem. The fix isn't just "remove the iframe" — it's "server-render the content, whether it's on the main page or the endpoint the iframe points to."


The part I got wrong

For a while, I assumed the main problem was "performance" in the generic sense.

So I looked for the usual suspects: network latency, image sizes, CDN headers, cache behavior. Those mattered — but they were not the whole story.

The deeper issue was architectural shape.

The app had too many responsibilities, too much shared code, and too many surfaces crammed inside one deployment unit. The iframe didn't create this problem—it revealed it by making us instantiate our monolith multiple times. It exposed one big system that we had been pretending was several smaller ones.

That was the mistake: blaming the messenger instead of the message.

I had to admit I didn't have the answer yet — and then ask for time to fix the system instead of just firefighting its symptoms.

So I went to my manager with a proposal: give the site a diet.

Not a vague "let's optimize." A real plan.

  • reduce the bundle
  • remove unused modules
  • split pages into smaller pieces
  • improve the build pipeline
  • stop making every page carry the weight of every other page

That conversation mattered as much as the code changes.

Sometimes ownership starts when you stop waiting for permission to understand the system.


What actually helped — and what we didn't do yet

I want to be honest about the scope here.
We didn't tear the monolith down completely. We didn't migrate to micro-frontends or rebuild from scratch. What we did was make the system smaller, cleaner, and easier to reason about — so that the next architectural decision wouldn't feel impossible.

Think of it as load-bearing surgery before the real renovation.

1. Tree shaking and dead-code removal

We stripped away modules that were bundled but never used.

This sounds mundane, but dead code isn't harmless. It still has to be discovered, bundled, transmitted, parsed, and often evaluated. Every unused import is a small tax on runtime.

2. SWC for faster builds

We introduced SWC to improve build speed.

That didn't just save developer time. It changed behavior. When builds are slow, people batch changes, avoid refactors, and hesitate to split code. When builds are fast, iteration feels cheaper. That makes architectural cleanup more realistic.

SWC didn't directly shrink our bytes, but it gave us the 10x faster build loops we needed to aggressively experiment with code-splitting without losing our minds.

3. Page splitting and better code splitting

Instead of making every page carry the whole app, we split features into smaller delivery units.

The rule was simple: if a user doesn't need a feature to view the current page, don't make them download it now.

This is where the unit of deployment matters. If the app surface is large, the bundle shouldn't pretend otherwise.

The results

The transformation wasn't subtle.

Deployment time dropped from 10 minutes to about 2.

Loading time fell from more than 3 seconds to under 1 second.

Those aren't vanity metrics. They change how teams work. Faster deploys mean faster feedback. Faster feedback means more confidence. More confidence leads to better architecture decisions because people are no longer afraid to touch the code.

And better architecture decisions compound.


The lesson: optimize by shrinking what you ask the browser to carry

Here's the real takeaway.

If your app is big enough to contain multiple product experiences, promotion systems, and routing behaviors, then embedding it via iframe for modular reuse is usually the wrong default—not because iframes are bad, but because they force you to pay the full cost of your monolith multiple times.

Not because iframes are inherently bad.

Not because SPAs are inherently bad.

But because a bloated monolith embedded multiple times on one page creates a multiplicative performance disaster.

A better architecture usually does one or more of these:

  • serves distinct product experiences as distinct pages with properly split bundles
  • uses SSR or prerendering where crawlability matters
  • splits bundles by route and by feature so no page carries weight it doesn't need
  • keeps iframes for truly isolated embeds with minimal shared dependencies, not as a way to reuse a full application
  • reduces shared runtime between unrelated surfaces

The goal is simple: make the browser load only what the user needs, let search engines see what matters, and make failures local instead of global.

That's the bet I'd make again.

Not that every frontend should be micro-frontends.

Not that every SPA should be thrown out.

But that when your app starts behaving like a whole ecosystem, the right move is to make the monolith small enough that embedding it doesn't break your performance.

Conclusion

The hardest part of this project wasn't the bundle size or the deployment time. It was admitting that the architecture was making the load-time slower and the product worse.

Once I stopped treating the problem as a series of isolated bugs, I could see the pattern: too much code, too much coupling, and an iframe strategy that multiplied those sins instead of solving them.

I used AI assistance for this post, but the experiences and ideas are genuine.

Top comments (0)