Site performanceWeb development

Passengers on every page: a whole language's dictionary, a second header, and a chat nobody asked for

On 5 September 2026 we took our own site's response apart line by line and found five things that shipped to every visitor's browser although nobody used them. On 13 September a sixth turned up. Here are the bytes before and after, a lesson about characters versus bytes, and five checks for your own site.

We have written about our own site's speed twice. First — why a page thought for three seconds: a heavy menu query that ran on every view. Then — compression that supposedly worked; the second half of that article shows that the weight of a modern page sits in the serialized data for components, not in the markup. We are not repeating that point here.

This article asks something else: what exactly ships to the browser along with every page although the visitor will never use it. On 5 September 2026 we took our own site's response apart and found five such passengers. On 13 September a sixth turned up. On 16 and 17 September we checked what was left of all this — and saw that one passenger is already creeping back.

What we found on 5 September

The commits from that day record it: a page carried 300–380 KB of React data, and the largest chunks had nothing to do with the page itself.

A whole language's dictionary. All interface translations shipped to the browser on every page, although the components that run in the browser read 19 of the dictionary's 40 sections. The rest — the “About us” texts, the legal pages, utility labels — are translated by the server, and the browser does not need them.

A second header. The framework puts a 404 page shell into every response, in case an address turns out to be missing during a navigation. Our 404 page drew the full header with the services menu, so every page carried another 18 KB that nobody ever saw.

The article's text three times over. The table of contents was assembled by two components in the browser, and each of them received the article's entire HTML in order to find the headings in it. The text arrived once to be read and twice more for the sake of a list of a few items.

The mobile menu on a desktop. The contents of the mobile menu — the section buttons, categories, search, 29 icons — took up 17 KB of markup and about 150 DOM nodes. The server drew it on every page and hid it with transparency, including for people who opened the site on a desktop, where this menu is not shown at all.

A cover image the browser learned about late. A single trace run of the article on a mobile viewport (Slow 4G, CPU slowed down fourfold) gave the largest element of the first screen at 1,094 ms. Of that, for 184 ms the browser simply did not know about the cover image: it was a background in the stylesheet and only showed up after the styles had loaded. Another 574 ms went on the original 1200×630 JPEG weighing 44–85 KB, with no conversion for the screen at all.

The sixth passenger, 13 September. Our chat script — 57 KB plus its settings and an open connection — inserted itself on the first interaction with the page, or simply when the page sat idle. In practice every visitor got it, not the one who clicked on the chat.

Why nobody noticed

None of these passengers is visible. The dictionary is not displayed, the 404 shell is not drawn until it is needed, the mobile menu is transparent, the chat looks like a button. Meanwhile the page looks right and works right, and unnecessary work that works correctly does not announce itself. The same pattern as the heavy menu query in the first article, only from the other side: there the invisible part sat on the server, here it sits in the response.

The second reason is the way this accumulates. Nobody decided to ship a whole language's dictionary: it was wired up once for the entire site, back when the dictionary was small, and then it grew along with the site. The 404 shell is put there by the framework, and the header went into it because that is what an ordinary page of the site looks like. Each decision on its own is sensible; together they add up to luggage.

What we changed

  • Only the 19 dictionary sections that the browser's components actually read now ship to the browser. A separate script checks the list: it walks the import graph from files marked 'use client' and fails if a section is missing. Without such a check a missed section only shows up on the live site — as a translation key instead of text.
  • The 404 page uses a light header with no menu.
  • The server assembles the table of contents items; a short array of headings goes into the props.
  • The mobile menu's contents mount after it is first opened. The article's server markup was left with 2 nodes instead of 156, and the links for search robots sit in the desktop menu panel, which stayed in the HTML.
  • The cover became an ordinary image with preloading and formats sized to the screen width.
  • The chat loads after a click on the button.

What we see on the live site

On 16 September we pulled three production pages with the same Accept-Encoding header Chrome sends, and took them apart with a single script.

Page HTML uncompressed Sent over the network Share of scripts carrying React data
home page 935,639 B 83,209 B 49%
blog article 453,910 B 59,273 B 65%
service page 633,694 B 66,207 B 61%

The header inside the 404 shell now takes 137 bytes with an empty list of services — against the 23,707 bytes of the real menu on the same page. The mobile menu's container in the article's markup is empty. The chat script is not mentioned in the page source at all: in its place there is a 707-byte button. The script itself weighed 56,953 bytes on 17 September, 20,214 compressed over the network — but only the person who clicked gets it.

The dictionary, though, has crept back to its old size, and there is a separate lesson in that.

A lesson about units of measurement

On 5 September a commit recorded “64 KB.” When we recounted that same dictionary file on the 16th, it came out at 63,261 characters — and 96,372 bytes. Cyrillic takes two bytes per letter in UTF-8, so the figure written down then described characters, not what actually travels over the network.

In bytes the picture looks like this. The full Ukrainian dictionary on 5 September was 96,372 B across 40 sections. The nineteen sections that are actually needed came to 57,976 B after the fix. Those same nineteen sections in the current branch are 67,924 B. In eleven days the dictionary grew because new blocks appeared on the home page: the Home section went from 12,852 to 20,539 bytes, 39 new keys, the largest of them a questions-and-answers block of 4,339 bytes.

That section arrives on every page because the home page's components read it. In the article page's response we checked all 53 of its strings longer than 40 characters: not one of them occurs anywhere else in that response. So a reader of the article receives 20.5 KB of home-page texts that are not on their screen. After brotli that is 5,553 bytes against 16,510 for all nineteen sections; the numbers were counted for each chunk separately, and inside a page they will be slightly different.

How to check this on your own site

1. How much data the page carries. For sites on Next.js with the App Router, open the page, the DevTools console, and run:

const d = self.__next_f.map(x => x[1] || '').join('');
new TextEncoder().encode(d).length

That is bytes. d.length will give you characters, and on a Cyrillic site you will undercount by roughly a third — exactly the way we did on 5 September. On Next.js with the Pages Router (without the App Router), look for the <script id="__NEXT_DATA__"> block instead of self.__next_f. In other frameworks, look in the page source for large <script type="application/json"> blocks that the framework inserts for hydration.

2. Whether someone else's text is riding along. Copy a sentence that exists only on the home page and search for it in the source of an article or product page (Ctrl+U, then Ctrl+F). If it is there but not on the screen, it is a passenger. Look the same way for popup texts, full lists of cities, reference tables, translations into other languages.

3. Whether hidden things are being drawn. In the inspector, find the mobile menu, modal windows, the cart — on the desktop version of the site. Hundreds of nodes before anything has been opened mean those nodes are built on the server and sent to everyone.

4. What loads before a click. The Network tab: reload the page, scroll a little and click nothing. The chat, video player and map scripts that show up in the list are what every visitor receives.

5. What the largest element of the first screen is. In the Performance tab, find the LCP. If it is an image set through background-image in the stylesheet, the browser learns about it late — that is exactly where we lost 184 ms.

What this did not fix

The dictionary grows along with the site, and a one-off trim does not hold by itself. To keep home-page texts off the articles, the dictionary would have to be split by page type, and in our version of the translation library a nested provider does not merge its sections with the parent one — every type would also have to carry the shared sections. We have not done this yet.

The header with the services menu tree still takes 23,707 bytes in every page's data. That menu is real, people use it, and there is no reason to remove it.

We did not separately measure when the cover appears after the change, so there will be no “after” figure here. All we know is that the browser now learns about it from the very beginning of the document, from the preload line. And the “before” figures — 1,094 ms and 184 ms — come from a single trace run on 5 September, not from the median of several runs: on an artificially slowed network and CPU such a measurement wanders from one run to the next.

The chat has a deliberate price: opening automatically after 25 seconds or on exit intent no longer works, because until the click there is no chat engine on the page.

And the limit of this whole exercise. These kilobytes matter on slow mobile internet and on weak phones, where they also have to be parsed; on a fast connection the difference is barely noticeable. The measurements were taken on one site, on three page types, on two specific days — this is our case, not a benchmark. If there is no time to do the same yourself, we do site speed-up and, separately, mobile speed with the same before-and-after measurements.

Tags

PerformanceSEO

Did you like the article?

Your opinion helps us create better content

Share with friends

Found something useful?

Help others learn about it - share the article on social networks

Thank you for helping us grow

Founder of LIONEX

Vladyslav Chystiakov

Writes about what he builds himself: online stores on OpenCart, applications on Next.js, integrations and site speed. The articles carry measurements and checks a reader can repeat on their own project, not general advice. Commercial development since 2015.

Questions

Frequently asked questions

Answers to common questions on the topic

What is the “page data” that ships to the browser along with the markup?

Modern frameworks hand over not just finished HTML but also a serialized description of the page — so that the browser can bring the interactive blocks to life without another request. This data sits in the same response, inside large utility scripts. On our three pages on 16 September 2026 it took up 49% of the home page's uncompressed HTML, 65% of the article page and 61% of the service page. That is exactly where the things a visitor will never see are hiding.

How can I quickly tell how much of this data my page carries?

Open the page you need and the DevTools console. On Next.js with the App Router a one-line measurement will do: collect the contents of all the utility blocks and measure them with TextEncoder — that gives you bytes. The length of the string in characters undercounts by roughly a third on a Cyrillic site, because Cyrillic takes two bytes per letter in UTF-8. We tripped over this ourselves: the “64 KB” written down on 5 September turned out to be 63,261 characters and 96,372 bytes.

Will it hurt search that the mobile menu's contents are no longer drawn on the server?

In our case no, and for a specific reason: the same links sit in the desktop menu panel, which stayed in the HTML. The robot sees them there. If you remove a hidden menu without leaving the links anywhere else, the loss is real. Before such a change it is worth comparing the set of links before and after — we did compare, and the article's server markup was left with 2 nodes instead of 156 without losing a single address.

Why is it better to load the chat script after a click, and what is lost by doing so?

Until 13 September 2026 our chat script inserted itself on the first interaction with the page, or simply when the page sat idle, which meant practically every visitor received it. Now a 707-byte button lives on the page, and the script itself (56,953 bytes on 17 September, 20,214 compressed over the network) arrives only for the person who clicked. The price is deliberate: opening the chat automatically after 25 seconds or on exit intent no longer works, because until the click there is no chat engine on the page.

How noticeable are these kilobytes to a visitor at all?

They matter on slow mobile internet and on weak phones, where the data has to be not just downloaded but also parsed. On a fast connection the difference is barely noticeable. The largest element of the first screen is worth looking at separately: in our trace of the article (Slow 4G, CPU slowed down fourfold) it appeared at 1,094 ms, and for 184 ms of that the browser simply did not know about the cover image, because it was a background in the stylesheet. That is already tangible.

Get the best articles by email

Subscribe to our newsletter and receive useful tips, insights and news about web development, marketing and business.

We respect your privacy. You can unsubscribe at any time.

More from the blog

Related articles