Pick any page on our site and count a string that appears exactly once on it. Our music quiz page shows 34 questions, each with one answer, each answer prefixed with the word "Answer:".
curl -s https://pub-trivia.app/quiz-questions/music | grep -o 'Answer:' | wc -l
# 68
Sixty-eight. Everything on that page is in the response twice.
This is not a bug and most people reading this already know why, but almost nobody has measured it, and the measurement is more interesting than the explanation.
Where the second copy lives
A React Server Components page sends two representations of the same tree. The first is HTML, which the browser paints. The second is the flight payload, a serialised description of the component tree that React reads during hydration, and it arrives inside a series of inline scripts:
<script>self.__next_f.push([1,"...a few thousand escaped characters..."])</script>
That music page contains 18 of those calls. Add up only the string literals inside them and you get 55,931 characters out of a 92,215 byte response. The second copy is 60.7 percent of the page.
Running the same measurement across a few of our pages:
| Page | Response | Flight payload | Share |
|---|---|---|---|
/quiz-questions/music |
92,215 B | 55,931 B | 60.7% |
/tools/team-name-generator |
80,384 B | 47,805 B | 59.5% |
/features/quiz-builder |
58,661 B | 34,742 B | 59.2% |
/faq |
77,403 B | 43,822 B | 56.6% |
/pricing |
100,853 B | 57,074 B | 56.6% |
/ (homepage) |
150,919 B | 73,148 B | 48.5% |
Text-heavy pages sit around 60 percent. The homepage is lower not because it duplicates less but because it has more markup that is not text: a product mock made of several hundred elements, whose class names appear in the HTML and whose structure appears in the payload in a more compact form.
So, by raw byte count, more than half of every page we serve is a copy of the other half. That sounds unacceptable until you measure what it costs on the wire.
What it costs after compression
Vercel serves these with brotli. Ask for it explicitly and count what actually arrives:
curl -s -o /dev/null -H "Accept-Encoding: br" \
-w "%{size_download}\n" https://pub-trivia.app/quiz-questions/music
# 12521
92,215 bytes of HTML become 12,521 bytes on the network. That is a 7.4x reduction, which is much better than text normally compresses, and the reason is precisely the duplication: a compressor's whole job is to notice that it has seen this run of bytes before.
To find out what the second copy actually costs, strip the flight scripts out of the saved response and compress both versions:
const stripped = html.replace(/<script>self\.__next_f\.push\([\s\S]*?\)<\/script>/g, "")
zlib.brotliCompressSync(Buffer.from(html)).length // 9884
zlib.brotliCompressSync(Buffer.from(stripped)).length // 5922
So the flight payload is 60.7 percent of the raw bytes and about 40 percent of the compressed ones: 3,962 bytes on this page. Under gzip the same comparison is 14,365 against 7,091, a cost of 7,274 bytes.
Two things worth taking from that pair of numbers.
The optimistic reading, "it compresses to nothing, ignore it", is wrong. It compresses well, not away. Four KB is a real number on a phone on pub WiFi, which is a connection we think about more than most people because our players are standing in one.
The pessimistic reading, "half my page is waste", is also wrong, and it is the one that sends people chasing the problem with the wrong tool. If you look at a 60 percent figure and start converting server components to client components to get rid of it, you will trade 4 KB of highly compressible duplicated text for a larger JavaScript bundle that has to be parsed and executed, and the page will get slower.
What actually changes the number
The payload is a function of how much tree there is, so the only real lever is how much content one URL holds.
We publish 462 quiz questions. They live on fourteen category pages rather than one long page, and that was a reader decision: somebody planning a music round does not want to scroll past the geography questions. The byte arithmetic happens to agree. Thirty-four questions plus their surrounding prose is a 92 KB response and a 12.5 KB transfer. The same library on a single URL would be roughly thirteen times the question content, with the duplication applying to all of it, and the wire cost would land somewhere in the hundreds of kilobytes for a page where most readers want one section.
That is the actionable version of the finding. The duplication is not something you remove, it is a multiplier on page size, which means it turns "how much content belongs on one URL" from a content question into a performance question. A 2 MB page of text in a server-rendered React app is not a 2 MB problem.
The other place the number stops mattering entirely is client-side navigation. Click an internal link and Next fetches the flight payload on its own, with no HTML at all, because the browser already has a running React tree to update. The double cost applies to the first load of a session and to nothing after it.
Try the measurement on your own pages
It is three commands and worth knowing for your own app:
# raw response
curl -s https://your.site/page | wc -c
# what actually crosses the network
curl -s -o /dev/null -H "Accept-Encoding: br" -w "%{size_download}\n" https://your.site/page
# how many flight chunks there are
curl -s https://your.site/page | grep -o 'self.__next_f.push' | wc -l
If the compressed number is small, the raw percentage is trivia. If the compressed number is not small, you have learned that your page is too big, which was true before you knew why.
Pages to point curl at
- A quiz question page is the text-heaviest shape we have, 34 questions and answers, and the one in the table above
- The team name generator ships 110 names as text plus an interactive widget, so it is the mixed case
- The homepage is the markup-heavy case, which is why its share is the lowest on the list
All public, no account needed. The app behind them runs a pub quiz off the phones in the room, free tier, no card.
Top comments (0)