惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Cisco Talos Blog
Cisco Talos Blog
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
爱范儿
爱范儿
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
Jina AI
Jina AI
雷峰网
雷峰网
The Register - Security
The Register - Security
The Cloudflare Blog
博客园 - 【当耐特】
M
MIT News - Artificial intelligence
I
InfoQ
博客园 - 三生石上(FineUI控件)
H
Help Net Security
博客园 - 司徒正美
Vercel News
Vercel News
WordPress大学
WordPress大学
S
SegmentFault 最新的问题
云风的 BLOG
云风的 BLOG
B
Blog
Google DeepMind News
Google DeepMind News
B
Blog RSS Feed
L
LangChain Blog
人人都是产品经理
人人都是产品经理
GbyAI
GbyAI
T
The Blog of Author Tim Ferriss
T
Tailwind CSS Blog
aimingoo的专栏
aimingoo的专栏
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
Recorded Future
Recorded Future
小众软件
小众软件
Martin Fowler
Martin Fowler
罗磊的独立博客
Stack Overflow Blog
Stack Overflow Blog
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
酷 壳 – CoolShell
酷 壳 – CoolShell
腾讯CDC
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
Apple Machine Learning Research
Apple Machine Learning Research
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
博客园 - Franky
Hugging Face - Blog
Hugging Face - Blog
Y
Y Combinator Blog
V
Visual Studio Blog
F
Fortinet All Blogs
Microsoft Azure Blog
Microsoft Azure Blog
大猫的无限游戏
大猫的无限游戏
Engineering at Meta
Engineering at Meta
N
Netflix TechBlog - Medium
V
V2EX
Blog — PlanetScale
Blog — PlanetScale

Echo JS

Desktop apps Introducing mermaid-lint: Stop Shipping Broken Diagrams The quiet problem with unnecessary async - Matt Smith GitHub - ant-design/ant-design-cli: Ant Design on your command line. Query component knowledge, analyze project usage, and guide migrations — fully offline. Uncovering the Magic Behind Playwright React Performance Isn’t About useMemo — It’s About Render Boundaries T2 No Escape Hatches · Prickles GitHub - auto-agent-protocol/auto-agent-protocol: Automotive Agent Protocol GitHub - coactionjs/coaction: Zustand-style state management with built-in render tracking and cached computed state GitHub - williamtroup/Rattribute.js: ❓ A lightweight JavaScript library for automatically changing HTML element attributes based on responsive screen sizes. GitHub - williamtroup/Rink.js: 🔗 A JavaScript library for generating responsive HTML link targets. SVAR Kanban: Interactive Task Board Component for Svelte, React, and Vue Performance Cost of Popular 3rd Party Scripts date-light - Tiny date utilities for JavaScript When React Hooks Stop Scaling: Moving Complex State to Zustand - Oren Farhi GitHub - jskits/loggerjs: A faster, more powerful isomorphic logger Out Loud — Free AI Text to Speech Pocket DB — Embedded single-file NoSQL for Node.js billboard.js 4.0 release: Canvas rendering mode, 94.3% faster! GitHub - thegruber/linkpeek: Secure TypeScript link preview and URL metadata extractor for Open Graph, Twitter Cards, JSON-LD, Node/Bun/Deno/edge. GitHub - evoluteur/healing-frequencies: Play the healing frequencies of various sets of tuning forks: Solfeggio, Organs, Mineral nutrients, Ohm, Chakras, Cosmic octave, Otto, DNA nucleotides... or custom. Animated sine waves - 27 lines of pure JavaScript Framework | Neutralinojs GitHub - AllThingsSmitty/typescript-tips-everyone-should-know: ✅ A curated collection of practical TypeScript patterns that improve safety, readability, maintainability, and developer experience. 🧠 Heat.js : JavaScript Heat Map GitHub - iDev-Games/State-JS: State.js is a CSS‑reactive framework that makes UI state and updates flow through CSS instead of JavaScript logic. GitHub - yankouskia/gameplate: :video_game: Boilerplate for creating game with WebGL & Redux :game_die: GitHub - yankouskia/get-browser: 💻 Lightweight tool to identify the browser (mobile+desktop detection)📱 GitHub - yankouskia/is-incognito-mode: Identify whether browser is in incognito mode 👀
How We Cut Slow Responses by 80% Migrating to Next.js App Router
Francesca Milan · 2026-06-16 · via Echo JS

Subito is Italy's leading classifieds marketplace, serving millions of users every month. Our ad detail page is one of the most heavily trafficked pages on the platform, handling thousands of requests per minute and playing a critical role in both SEO and revenue.

In this article, we'll walk through how we migrated it from the Next.js Pages Router to the App Router, the challenges we encountered along the way, and how the migration helped reduce slow responses by roughly 80%.

Table of Contents

  • The Goal
  • Migration Approach: Ship Without Blocking Anyone
  • Problem 1: HTTP 410 Gone in App Router
  • Problem 2: HTML Streaming Behind Nginx and Akamai
  • Rollout and SEO Monitoring
  • How Much Effort Did This Migration Require?
  • Results
  • Conclusions

The Goal

As we always do at Subito, we never migrate for migration's sake. We go in with specific, measurable targets:

  • Faster page loads: leverage React Server Components and HTML streaming
  • Better SEO: improve page speed scores, working alongside our SEO team, faster pages rank better

The migration touched every layer of the stack: Next.js, a custom Express server, Nginx, and Akamai as our CDN. Here's what each step looked like.

Migration Approach: Ship Without Blocking Anyone

The ad detail page is one of the highest-traffic pages on the platform. We couldn't freeze product work for weeks while we rewrote it. So we didn't.

Instead of doing a big-bang rewrite, we incrementally added the App Router tree, server components, data fetching, layouts, alongside the existing Pages Router page.

The key insight was that each new server component was simply a wrapper around the same client component already used by the Pages Router.
There was no duplication: one client component, two consumers.
This meant that any bug fix or product feature added to a client component automatically applied to both the old and new page. Developers working on the Pages Router side were already contributing to the App Router version without knowing it.

This approach gave us two things:

1. Clean component boundaries. We used server components to own data fetching and pass only what client components actually need. The classic prop-drilling waterfall disappeared.

2. A defined Data Access Layer. Following the Next.js recommendation, we centralized all data fetching behind a DAL and used React's cache() to deduplicate calls. When three server components all need the same ad data, the network call fires once.

For sections with heavy data dependencies or secondary content, we combined Suspense with the use() hook to unlock streaming:

// GeneralInfo.tsx
const GeneralInfo = ({ id, ...props }: Props) => {
  const { promise, resolve } = Promise.withResolvers<AsyncData>();

  fetchAdItem(id).then((ad) => {
    const { urn, advertiser } = ad;

    Promise.all([
      fetchShopData(advertiser.shopId, advertiser.type),
      // ...all the other fetches
    ]).then(([shop /* ...other results */]) => {
      resolve({ ad, shop /* ...other props */ });
    });
  });

  return (
    <Suspense fallback={<GeneralInfoSkeleton />}>
      <LazyGeneralInfo loadData={promise} {...props} />
    </Suspense>
  );
};

const LazyGeneralInfo = ({ loadData, ...props }: Props) => {
  const data = use(loadData);
  // ClientGeneralInfo is the same component used by the Pages Router version. No duplication!
  return <ClientGeneralInfo {...data} {...props} />;
};

The skeleton renders immediately while data resolves in the background. Once it arrives, React swaps in the real component, no layout shift, no full-page loading state.

Progressive Rollout via Custom Express Server

We run a custom Express server in front of Next.js. This gave us a clean toggle to route traffic between the old and new page:

// server.ts
server.get(
  adDetailListingPath,
  validateAdDetailListingParams,
  clientHints,
  securityMiddleware,
  (req, res) => {
    if (enableAppRouterForAdDetail(req.query, req.params.category)) {
      return app.render(req, res, '/ad/', req.params);
    }
    return app.render(req, res, '/addetail/', req.params);
  }
);

// utils/rollout.ts
const FORCE_MIGRATED_AD_QP = 'use-ar';
const MIGRATED_CATEGORIES: Array<string> = ['job'];

export const enableAppRouterForAdDetail = (
  query: ParsedQs,
  pathFriendlyCategory?: string
) => {
  return (
    query[FORCE_MIGRATED_AD_QP] === '1' ||
    (pathFriendlyCategory && MIGRATED_CATEGORIES.includes(pathFriendlyCategory))
  );
};

?use-ar=1 forced the new page for internal testing. The category list let us roll out one macro-category at a time in production, starting with Job category and expanding progressively.

Problem 1: HTTP 410 Gone in App Router

The ad detail page is indexed by search engines, so when an ad expires we must return a proper 410 Gone response. A redirect or even a standard 404 Not Found is not sufficient: a genuine 410 clearly signals to crawlers that the page has been permanently removed and should be dropped from the index as quickly as possible.

The App Router, at the time of our migration, had no built-in API for 410. notFound() returns 404. There's no gone().

We wrote a full discovery article on the available workarounds here: How to Return HTTP 410 Gone in Next.js App Router — Two Workarounds

Since we already owned the Express layer, we went with a status code interceptor: Express intercepts the outgoing response before it hits the client and rewrites the status code when the page signals a gone state. No framework hacks, no monkey-patching Next.js internals.

Problem 2: HTML Streaming Behind Nginx and Akamai

This was the trickiest part of the whole migration: during staging tests we noticed our <Suspense> skeletons were invisible. We even forced an artificial 5-second delay on data fetching; the skeleton still never appeared. The page just loaded slowly as a blank screen.

Nginx Buffering

The first fix was disabling response buffering in Nginx, as documented by Next.js:

proxy_buffering off;

With buffering off, the skeleton appeared... but only when hitting the origin server directly. Through the CDN, still nothing.

Akamai Buffering

We shifted our investigation to Akamai. Our initial hypothesis was a caching layer conflict or a compression issue. We disabled caching... no effect.

Before diving deep into Akamai's Property Manager, we opened a support ticket. The answer was clear:

By default, Akamai buffers HTML responses before delivering them to the client. Disabling this behavior requires advanced metadata and cannot be configured through standard Property Manager rules.

Chunked transfer encoding and early flush are not available as standard toggles, but Akamai provided us with a custom behavior that we could apply via Property Manager:

<network:http.buffer-response-v2>off</network:http.buffer-response-v2>
<edgeservices:wco.input-buffer-minimum>0B</edgeservices:wco.input-buffer-minimum>
<edgeservices:wco.request-flush>on</edgeservices:wco.request-flush>
<edgeservices:modify-outgoing-response.chunk-to-client>on</edgeservices:modify-outgoing-response.chunk-to-client>

Once applied, streaming worked end-to-end. Skeletons appeared immediately; content streamed in as data resolved :tada!

Streaming + Caching: What Actually Happens

After enabling the custom behavior, we re-enabled the Akamai cache layer (non-negotiable for a high-traffic site like ours) and observed the following behavior:

  • Cache MISS: The HTML is streamed. No Content-Length header in the response, the browser receives chunks progressively.
  • Cache HIT: The full HTML is returned at once with a Content-Length header (e.g., 28315). Streaming doesn't apply, but the page is already fully built so it doesn't need to be.

This is the correct behavior. Cached responses are fast regardless; streaming only matters for the cold path.

Rollout and SEO Monitoring

We rolled out in two distinct phase, deliberately separated, not just for technical safety, but for SEO safety.

Phase 1 was controlled at the Express layer: migrate to App Router, streaming off. We started with a single category (Job), monitored carefully, then expanded one macro-category per cycle until all traffic was on the App Router.

Only once the migration was stable did we consider enabling HTML streaming. We were genuinely concerned about the SEO implications: would Google's crawler handle a chunked, incrementally-delivered HTML response correctly? Would it see the full page content, or just whatever arrived in the first chunk? We didn't want to find out by watching rankings drop.

Phase 2 enabled HTML streaming: we started with the Videogiochi (Video Games) category, using a targeted rule in Akamai Property Manager:

If Path matches one of: /vi/*, *.htm
And Path matches one of: /videogiochi/*

Our SEO team played a key role throughout the rollout: before enabling streaming in a new category, we monitored its SEO metrics for roughly two weeks, ensuring there were no unexpected impacts on crawling, indexing, or search performance. Only after the category showed stable results did we move on to the next one.

How Much Effort Did This Migration Require?

Surprisingly little!
The migration was carried out primarily by a single developer, working on it incrementally between regular feature deliveries rather than as a dedicated full-time initiative.

A significant enabler was Claude Code, which we used extensively throughout the project. It helped with planning, architectural exploration, implementation details, and routine development tasks, allowing us to move faster while keeping the migration manageable for a very small team.

Results

Response Times

The Grafana graph below shows the percentage of responses taking more than 250ms. Before the migration (up to ~03/25), the figure sat consistently between 25% and 40%, with spikes reaching 55%.
After the rollout, it collapsed to near zero and stayed there.

SEO Performance

The data below shows Fast URLs (< 500ms) across categories:

The improvements are significant across the board, with Video Games showing the strongest gain at nearly 30%.

Conclusions

The migration itself wasn't the hardest part: Next.js made it possible to run Pages Router and App Router side-by-side, allowing us to migrate incrementally without stopping feature development.

The real challenge was making the entire delivery chain work together: React Server Components, streaming, Express, Nginx, Akamai, observability, and SEO requirements.

Only when all those pieces aligned did we unlock the performance improvements we were aiming for.

A few takeaways for teams considering a similar migration:

  • Parallel routing lets you migrate without a freeze. Pages Router and App Router coexist in the same Next.js project. Use that.
  • A custom server layer is an asset. Express gave us per-request routing control, progressive rollout toggles, and the ability to solve framework gaps (like 410 status codes) without waiting for upstream fixes.
  • HTML streaming requires work at every layer. Nginx buffering, CDN buffering, and chunked transfer encoding all need explicit configuration. Don't assume streaming works end-to-end just because your Next.js code is correct.
  • End-to-end and visual regression tests caught regressions early. Before touching production we had confidence from automated test coverage.