PixStatic All articles
Web Development

Your Framework Is Lying to You About Speed

PixStatic
Your Framework Is Lying to You About Speed

Photo by Photo by path digital on Unsplash on Unsplash

There's a seductive pitch that comes bundled with every modern JavaScript framework: more capability, better developer experience, and — somehow — great performance too. It's a compelling package. But once you start pulling apart real-world waterfall charts and squinting at Core Web Vitals scores, a messier picture emerges.

Dynamic rendering isn't inherently bad. But the hidden tax it levies on your users? That part rarely makes it into the conference talk.

What the Waterfall Actually Shows

A waterfall chart is brutally honest. It doesn't care about your DX preferences or your team's familiarity with React. It just shows you what the browser is doing, in order, and how long each step takes.

For a dynamically-rendered page — think a Next.js app running in SSR mode, or a client-side-rendered React app — that waterfall typically includes:

Even with aggressive code-splitting and lazy loading, you're stacking network round trips. Each one adds latency. On a fast fiber connection in San Francisco, that might be imperceptible. On a 4G connection in rural Texas, or on an aging Android device? Those milliseconds compound fast.

Contrast that with a statically-generated page from Hugo or a Gatsby build. The HTML arrives complete. There's no hydration penalty, no waiting for JavaScript to decide what to render. The browser gets real content immediately and starts painting.

The Numbers That Don't Lie

Core Web Vitals give us three metrics worth obsessing over: Largest Contentful Paint (LCP), Interaction to Next Paint (INP), and Cumulative Layout Shift (CLS).

In controlled testing comparing equivalent content pages — same images, same copy, same basic layout — the differences between rendering strategies are stark:

LCP is where static generation consistently wins. When the HTML is pre-built and served from a CDN edge node, LCP times frequently land under 1.5 seconds even on mid-tier mobile connections. SSR pages, depending on server response time and hydration overhead, often push past 2.5 seconds. Client-side rendered apps can hit 4+ seconds before meaningful content appears.

INP is where things get interesting. Heavily hydrated pages — ones where React or Vue is taking over a fully server-rendered DOM — can produce surprisingly poor interaction responsiveness during that hydration window. Click something too early and nothing happens. That's not a bug, it's the architecture.

CLS is the sneaky one. Dynamic content that loads after initial paint — ads, personalized recommendations, late-resolving API data — causes layout shifts that users register as jank even if they can't name what's wrong. Static pages, by definition, have their layout fully resolved before the browser ever sees them.

Next.js, Gatsby, and Hugo Walk Into a Performance Test

Let's be specific, because frameworks aren't monoliths.

Next.js is genuinely powerful, and its hybrid rendering model (static generation per-page, SSR where needed) is the right idea. The problem is that the default mental model for many Next.js teams skews toward SSR because it feels safer — you always have fresh data. The result is apps that could be 80% static but aren't, because nobody audited the tradeoffs.

Next.js apps with proper getStaticProps usage and ISR (Incremental Static Regeneration) can match Hugo on performance. But that requires deliberate architecture decisions, not just installing the framework.

Gatsby made a strong bet on static generation early, and its performance story is genuinely good — when the build process doesn't become the bottleneck. Large Gatsby sites with thousands of pages can have build times that stretch into tens of minutes, which is its own kind of tax. The runtime performance, though? Solid. Gatsby's image optimization and prefetching are legitimately well-engineered.

Hugo is almost unfairly fast. Written in Go, it builds thousands of pages in seconds. There's no JavaScript runtime overhead at all unless you add it yourself. For content-heavy sites — documentation, blogs, marketing pages — Hugo consistently delivers LCP scores that make framework-heavy alternatives look sluggish by comparison. The tradeoff is real: less dynamic capability by default, a steeper learning curve for developers coming from JS-land.

The Counter-Intuitive Finding

Here's what the data keeps surfacing: dynamic rendering penalizes the users who can least afford it.

Power users on fast machines with high-bandwidth connections barely notice the difference. Your framework's JavaScript overhead on a MacBook Pro connected to gigabit ethernet is basically invisible. But the median American internet user isn't that person. They're on a mid-range phone, on a variable LTE connection, potentially with a dozen other browser tabs open.

For those users, the gap between a pre-rendered static page and a dynamically-rendered one isn't academic. It's the difference between engaging with your content and bouncing before LCP fires.

Google's own data consistently shows that as LCP increases past 2.5 seconds, conversion rates and engagement drop measurably. If your framework is adding 1-2 seconds of rendering overhead for pages that don't actually need dynamic data, that's not a feature — it's a liability.

When Dynamic Rendering Is Actually the Right Call

This isn't a blanket indictment. Dynamic rendering earns its complexity when:

The mistake isn't using dynamic rendering for those cases. The mistake is defaulting to dynamic rendering for everything because it's what the framework makes easy.

What You Should Actually Do

Audit your routes. Seriously — open your Next.js or Nuxt app and go page by page. Ask: does this page actually need to be dynamic? If the answer is "not really, but it's easier this way," that's a performance debt you're carrying silently.

Push static as far as it goes. Use ISR or similar patterns to get cache freshness without full SSR overhead. Treat truly dynamic routes as exceptions, not defaults.

Measure with real devices. Chrome DevTools throttling is useful, but testing on an actual mid-range Android device over a real mobile connection is revelatory. The discomfort of watching your app struggle is good motivation to fix it.

Your framework isn't lying to you on purpose. But it's not going to volunteer the performance cost either. That part's on you to find.

All Articles

Related Articles

The Compression Trap: When Smaller Images Actually Break Your Site

The Compression Trap: When Smaller Images Actually Break Your Site

Static Sites Are Having Their Moment — and This Time, It's Not a Fad

Static Sites Are Having Their Moment — and This Time, It's Not a Fad

Stop Chasing Pixel-Perfect: The Responsive Design Standard Nobody Can Actually Meet

Stop Chasing Pixel-Perfect: The Responsive Design Standard Nobody Can Actually Meet