When the HTML Payload Itself Is the Problem

Most performance work targeting React applications focuses on trimming JavaScript bundles. But for a data-heavy travel platform serving thousands of SEO landing pages, the fight was won much earlier in the pipeline — inside the HTML document itself. Bookaway, a ground transportation marketplace operating across 1,500 operators and 20,000 services in 63 countries, discovered that its landing pages were shipping hundreds of kilobytes of redundant JSON inside the markup. That payload directly impacted both real users and Google's ability to index tens of thousands of pages efficiently.

The core issue traces back to how server-side rendering works in Next.js. When a request hits the server, data fetched in getServerSideProps is used to construct the HTML. But the same data must be embedded in the document so that React can re-build the component tree during client-side hydration. Next.js handles this by serializing the props object into a script tag with the id __NEXT_DATA__. If that props object is bloated, the HTML document carrying it becomes bloated too.

Four Landing Page Types, One Shared Bottleneck

Bookaway's need for scale comes from how travelers search. Users rarely look for a specific route ID; they search for natural queries like "Bangkok to Pattaya." This led to four distinct types of SEO-focused landing pages:

  • City A to City B: routes between two specific cities (e.g., Bangkok to Pattaya).
  • City: all lines passing through a single city (e.g., Cancun).
  • Country: all lines serving a country (e.g., Italy).
  • Station: all lines serving a specific station (e.g., Hanoi Airport).

Every one of these page types depends on rich backend data. The graph of relationships — lines, stations, suppliers, schedules, pricing — is genuinely complex. The problem wasn't the amount of data in the database. The problem was how much of it was flowing through to the client's HTML.

Two Architectural Defects

Analyzing a station page with a JSON size analyzer exposed two distinct defects in the data flow:

1. Unaggregated data. The JSON embedded in the page contained the complete, granular list of products — every bus line passing through that station, with all its underlying relationships. The UI didn't need most of this. It needed summaries derived from it, such as a unique list of supplier names. The aggregation work was happening on the client.

2. Irrelevant fields. Since data arrived straight from REST endpoints, it included fields that had no use in either the rendering or aggregation process. REST APIs return fixed response shapes; a service might include pictures or an id field that a given page component never references. On popular pages, this inefficiency compounded until the embedded JSON hit 200-300kb.

This created a redundant processing pipeline: React received raw REST output, aggregated it during the server render, then received the identical unaggregated data in the HTML for hydration, and performed the same aggregation again in the browser. The rendering work was being duplicated, and the HTML was carrying data far beyond what the page needed.

Layering in a Cleaner Pipeline

The fix didn't come from optimizing the existing flow. It came from restructuring the architecture into three stages before data ever reaches the page component:

  1. GraphQL layer that collocates field requirements with components. Bringing a GraphQL server between the REST APIs and the Next.js server means components request exactly the fields they need. When the underlying database gains new fields, that growth stops rippling through every HTML payload.
  2. A dedicated PageLogic function inside getServerSideProps. This function performs the aggregation and preparation of data for UI presentation. It runs only once, on the server, and transforms granular results into the compact, ready-to-render structure the components expect.
  3. Removing the dual-aggregation pattern. The client no longer performs data transformation. It only consumes the already-aggregated props, which means React's hydration step receives a far smaller object.

From Raw Objects to Render-Ready Props

A concrete example demonstrates the size difference. To render a section listing suppliers operating at a given station, the page previously queried all lines from the REST lines endpoint. The response contained irrelevant fields alongside the useful data. Sending that raw object through the old path meant every field, relevant or not, was factored into the HTML payload.

With the new architecture, that raw response first hits the GraphQL layer, shrinking out the fields no component uses. The result is a leaner object — but still not yet aggregated. It still represents individual line records when the UI needs only a distinct set of supplier entities.

That's the job of PageLogic, which receives the trimmed list and performs the reduction. The transform produces a much smaller structure — a handful of suppliers with only the attributes the UI renders. This minimised, ready-to-use object is what reaches the page component and ultimately gets serialized into the __NEXT_DATA__ JSON.

The outcome of this re-architecture was significant. Pages whose embedded JSON ran to 200-300kb came down to roughly the 5-15kb range — a reduction of over 90% on the most data-intensive pages.

Measuring Real-World Impact

Smaller HTML affects both user experience and SEO operations. For users, a smaller document starts painting sooner. LCP (Largest Contentful Paint) depends on the page's main content arriving fast, and an HTML payload dominated by a massive script tag is an obvious impediment. Google's documentation reinforces that making a site faster also increases crawl rate — a speedy response signals healthy servers to Googlebot, allowing it to retrieve more content over the same number of connections.

The team's measurement approach stays grounded in Chrome DevTools' network panel, checking the content download metric for the initial HTML resource under slow 3G throttling. That timing matters because the HTML transfer itself is the gate to everything else — rendering, hydration, and LCP. Trimming hundreds of kilobytes from that gate improves the experiences of real users on mid-range devices and slower networks, not just those on top-tier hardware.

The Performance Budget Argument

The reduction also restructures the project's performance budget. Broader web performance research suggests a practical budget caps sites at roughly ~100KiB of HTML/CSS/fonts and ~300-350KiB of gzipped JavaScript. When the HTML document devours a large part of that budget on its own, there is little room left for necessary CSS or JavaScript resources. The changes allowed Bookaway to redistribute that weight back to where it matters — actual page functionality and experience.

Aggregating data on the server, restricting fields at the API boundary, and shipping lean props to the client turned out not just to be code quality improvements, but core infrastructure for the scale of their landing pages. For teams running Next.js site with data-heavy getServerSideProps feeds, the lesson from Bookaway is straightforward: audit the JSON hiding inside the HTML, check whether the client is doing needless server data work, and identify the fields that never make it to the on-screen components. The parser, the serializer, and ultimately the users will thank you.

A Layered Approach to Payload Reduction

Solving the payload problem wasn’t just about tweaking Next.js configuration. The team introduced new architectural layers to handle data more efficiently:

  • GraphQL server: Added helpers for fetching precisely the fields required by the UI.
  • Dedicated aggregation function: Runs exclusively on the server, centralizing all data reduction logic.

These changes delivered benefits beyond raw performance gains. The new structure simplified code organization and made debugging noticeably easier:

  1. All logic for reducing and aggregating data now lives in a single, dedicated function.
  2. UI functions became far simpler—they receive ready-to-render data instead of performing their own data crunching.
  3. Server-side debugging improved, as REST endpoints no longer return unnecessary fields; only the required data is extracted.
Smashing Editorial