Optimizing Performance Without Falling for the RSC Hype
Today's nuggets – 2026‑09‑21
How Preply saved $200 k / yr and cut INP in half while staying on the Pages Router.
The React Server Components (RSC) and the new App Router have generated a lot of excitement. Many teams assume that moving to these features is the fastest way to improve Core Web Vitals, especially Interaction‑to‑Next‑Paint (INP).
Preply’s engineering team proved otherwise. By staying on the stable Pages Router and applying disciplined performance work, they reduced INP from ≈ 300 ms to ≈ 150 ms and saved ~ $200 k per year in cloud spend.
The takeaway is simple: measure, prioritize, and optimize the real bottlenecks before you adopt new abstractions. This article walks through the methodology, the concrete changes they made, and a checklist you can apply to any Next.js codebase.
1. Why the RSC/App Router Hype Is Misleading
| Claim | Reality |
|---|---|
| Server‑rendered components automatically reduce client‑side JavaScript | RSC only moves the rendering of some components to the server. The overall bundle size can stay the same or even increase if you add more server‑only data fetching. |
App Router replaces getStaticProps/getServerSideProps with a unified API |
The new fetch‑based data layer is powerful, but it also introduces streaming and edge‑caching complexities that require careful tuning. |
| Switching promises immediate Core Web Vitals gains | Vitals are driven by real user interactions, network latency, and critical rendering path. Without addressing those, a framework upgrade is just a cosmetic change. |
Preply’s engineers wanted hard data before committing to a migration. Their process is worth replicating.
2. Baseline: Measuring INP in the Wild
2.1 Instrumentation
-
Web Vitals SDK –
web-vitalsnpm package, reporting INP to a custom analytics endpoint. - Real‑User Monitoring (RUM) – Captured per‑page, per‑session data for the top 20 traffic‑heavy routes.
- Synthetic Lighthouse – Baseline for lab‑controlled comparison, not a decision driver.
import { onINP } from 'web-vitals';
onINP(metric => {
fetch('/api/collect-vitals', {
method: 'POST',
body: JSON.stringify(metric),
keepalive: true,
});
});
2.2 Findings
- Average INP = 298 ms across the 20 most visited pages.
- 70 % of the INP budget was consumed by long‑running JavaScript handlers (e.g., on‑click debounced search, analytics initialization).
- The remaining 30 % came from large layout shifts caused by late‑loading images and dynamic content.
The team set two concrete targets:
- INP < 180 ms for the top 20 pages (the threshold for “good” in Chrome UX Report).
- Reduce server‑side compute cost by at least 15 % (to hit the $200 k saving).
3. The Optimization Playbook
3.1 Trim the JavaScript Payload
-
Tree‑shake unused libraries – Enabled
next-plugin-optimized-imagesand rannpm dedupe. - Dynamic import for heavy UI widgets – Search box, rich text editor, and analytics were lazy‑loaded.
// pages/search.tsx
import dynamic from 'next/dynamic';
const SearchWidget = dynamic(() => import('../components/SearchWidget'), {
ssr: false,
loading: () => <p>Loading…</p>,
});
-
Remove polyfills – Modern browsers cover most features; disabled the
polyfillsflag innext.config.js.
module.exports = {
experimental: {
polyfillsOptimization: false,
},
};
Result: Bundle size fell from 2.8 MB to 1.9 MB gzipped, shaving ~ 80 ms off INP.
3.2 Prioritize Critical Rendering Path
-
Preload key fonts – Added
<link rel="preload" as="font" href="/fonts/inter.woff2" crossorigin>to_document.js. -
Inline above‑the‑fold CSS – Extracted the minimal CSS required for the hero section using
next-critical.
// pages/_document.js
import Document, { Html, Head, Main, NextScript } from 'next/document';
export default class MyDoc extends Document {
render() {
return (
<Html>
<Head>
<link rel="preload" href="/fonts/inter.woff2" as="font" type="font/woff2" crossOrigin="anonymous" />
<style dangerouslySetInnerHTML={{ __html: criticalCSS }} />
</Head>
<body>
<Main />
<NextScript />
</body>
</Html>
);
}
}
Result: First Contentful Paint (FCP) dropped 120 ms, which indirectly reduced INP because the UI became responsive sooner.
3.3 Optimize Data Fetching on the Pages Router
The team resisted moving to the App Router’s fetch‑based server components and instead refined the classic data‑fetching APIs.
| Issue | Fix |
|---|---|
getServerSideProps ran on every request, causing cold‑start latency on Vercel Edge Functions. |
Switched to Incremental Static Regeneration (ISR) where content was cacheable for 10 min. |
| Large payloads for the homepage (≈ 500 KB JSON). | Implemented selective field fetching on the GraphQL layer (query { lessons { id title } }). |
| Redundant calls to third‑party translation API. | Added server‑side memoization using lru-cache. |
// pages/index.tsx
export async function getStaticProps() {
const lessons = await fetchLessons({ fields: ['id', 'title'] });
return {
props: { lessons },
revalidate: 600, // 10 minutes
};
}
Result: Server‑side response time fell from 420 ms to 180 ms, eliminating a major INP contributor.
3.4 Image Delivery & Layout Stability
-
Next/Image with
layout="responsive"– Ensured intrinsic aspect ratios, eliminating layout shifts. - CDN‑based WebP conversion – Served AVIF/WebP when supported, falling back to JPEG otherwise.
import Image from 'next/image';
<Image
src="/images/hero.jpg"
width={1200}
height={630}
layout="responsive"
priority
placeholder="blur"
/>
Result: Cumulative Layout Shift (CLS) dropped from 0.18 to 0.04, removing its impact on INP.
3.5 Edge Middleware for A/B Experiments
Instead of moving the entire page to an RSC, the team used Edge Middleware to serve pre‑computed variants for a limited set of experiments.
// middleware.ts
import { NextResponse } from 'next/server';
export async function middleware(req) {
const url = req.nextUrl.clone();
if (url.pathname.startsWith('/search')) {
const variant = await getExperimentVariant(req.ip);
url.searchParams.set('variant', variant);
return NextResponse.rewrite(url);
}
return NextResponse.next();
}
Edge execution cost is ~ $0.000005 per request, negligible at scale, and it avoided a full migration to the App Router.
4. Financial Impact
| Metric | Before | After | Δ |
|---|---|---|---|
| Monthly compute (Vercel Serverless) | $28,400 | $20,500 | -$7,900 |
| Bandwidth (CDN) | 3.2 TB | 2.6 TB | -0.6 TB |
| Estimated annual savings | — | $200 k | — |
The savings stemmed mainly from ISR (fewer SSR invocations) and smaller payloads. The performance gains were a by‑product, not the primary driver of cost reduction.
5. When RSC & App Router Actually Make Sense
| Scenario | Recommended |
|---|---|
| Heavy data‑heavy pages where streaming is required (e.g., infinite scroll feeds) | App Router with fetch and use hooks. |
| Team wants to co‑locate data fetching with component logic and can invest in streaming SSR | RSC can reduce duplication, but only after baseline performance is acceptable. |
| Legacy codebase heavily reliant on Pages Router | Stick with Pages Router, optimize first. Migrate only if you need features like nested layouts that App Router uniquely provides. |
In short, adopt only when the feature solves a problem you already own, not because it’s the newest thing.
6. Practical Checklist for Your Next.js Project
- Instrument real‑user INP before any change.
- Audit JavaScript bundle – look for unused imports, large libraries, and polyfills.
-
Apply lazy loading to non‑critical widgets (
dynamicwithssr: false). -
Shift from
getServerSidePropsto ISR where data can be stale for ≤ 10 min. -
Enforce aspect ratios for every image (
layout="responsive"). - Preload fonts & inline critical CSS.
- Use Edge Middleware for lightweight request‑time variations instead of full SSR.
- Re‑measure INP after each step; stop when you reach your target.
- Only then evaluate whether RSC/App Router would provide additional value.
7. Actionable Takeaways
- Metrics first, hype second – Your migration budget should be justified by a measurable gap.
- Pages Router is still performant – Incremental static regeneration and disciplined bundling can rival App Router’s promises.
- Cost and performance are coupled – Reducing server‑side work often yields both lower spend and better INP.
- Iterative optimization beats big‑bang rewrites – Smaller, verifiable changes keep risk low and ROI high.
Conclusion
Preply’s experience shows that you don’t need React Server Components or the App Router to achieve world‑class interaction performance. By focusing on the fundamentals—payload size, critical rendering path, and efficient data fetching—the team cut INP in half and saved $200 k per year.
Use the checklist above to audit your own Next.js applications. Measure, prune, and only then consider a framework upgrade. The results will speak louder than any hype.
Source: “How Preply Improved INP on a Next.js Application Without React Server Components and App Router” – Preply Engineering
https://medium.com/preply-engineering/how-preply-improved-inp-on-a-next-js-application-without-react-server-components-and-app-router-491713149875
Top comments (0)