Site performanceWeb development

Why a page took three seconds: a query you can’t see in the page code

On 16 August 2026 our site responded in 3.05 s, while neighbors on the same server responded in 0.2–0.3 s. The culprit was a mega menu query that pulled 4.3 MB of translations on every page. We break it down along with three similar cases, and show how to find the same thing on your own site.

September 16, 2026
9 min read

On 16 August 2026 our own website responded in 3.05 seconds. Neighboring projects on the same server responded in 0.2–0.3 seconds. In other words, hosting had nothing to do with it: one machine, one network, a tenfold difference.

We found the cause not in the page code, but in a block that sits on every page and that nobody looks at. Below is a breakdown of that case and three similar ones we fixed by 5 September, with before and after numbers. At the end we show how to check the same thing on your own site in 10–15 minutes.

A header menu that weighed 4.3 MB

We measured not the page as a whole, but each data source separately. One query took 1,791 ms out of 2.4 seconds of total rendering. It was the query for the mega menu in the header, which means it ran on every page of the site: the homepage, an article, a service page.

The menu needs three short fields per item: a name, a heading and a short description. The query fetched 185 rows together with the translations column. And that column held not just the names, but the full text of every page in three languages: 23 KB per row, 4.3 MB in total. To read a few hundred bytes, the database returned megabytes every time.

The best detail: above this query sat the comment “light slice — without the heavy content JSON.” Literally, the comment was not lying. The field with the page text really was not selected on its own, but it lived inside the translation field, which was selected in full.

Why nobody noticed

Because this query is not in the page code. You open the article template, and there are only the article’s own queries. The menu lives in the header, the header in the shared layout, and when people look for why an article is slow, they look at the article.

Besides, the query was not wrong. It returned correct data, and the menu showed the correct items. A slow query that works correctly does not announce itself.

We fixed it in two steps. First, the needed fields began to be fetched by a separate query using specific paths inside the JSON, rather than the whole column. That query alone sped up from 1,891 to 107 ms. Then the finished menu tree was put into the data cache for an hour, with invalidation when a page is published in the admin panel. The second step matters more: with the cache, this query does not run at all on most page views.

The same pattern three more times

After the menu, we started looking for exactly these kinds of places: invisible from the page and identical for every view. We found three more.

Site settings, 23 August. Every render of any public page read the settings table 7–8 times: analytics tags (twice, in two different places), strings for structured data, the indexing flag, the list of enabled languages. These values are rarely changed from the admin panel, yet they were read on every request. That day we measured 0.46 s on a simple page and 0.72 s on a complex one, while a static file from the same server arrived in 0.30 s. We wrapped the reads in a cache with a 60-second lifetime and invalidation on save. Why the extra minute rather than invalidation alone: at least twelve places in the code can write to that table, and missing one is very easy. A cache without invalidation is worse than no cache, because an edit from the admin panel simply does not appear.

Service pages, 5 September. Pages in the service network responded in 0.45–0.5 s, section hubs in 0.17 s, blog articles in 0.36–0.43 s. The culprit was the “related services” block at the bottom of the page: it selected up to 60 neighbors together with their full translations. That came to 1.1–1.3 MB of JSON versus 65 KB for all the other page data, even though the links only need a name and a heading in one language. The same mistake as in the menu, just in a different place. Along the way it turned out that the row of the page itself was read twice: for the metadata and for the body.

Automatic links in articles, 5 September. Here the culprit was not the database but the processor. The profile showed that the single largest cost item on an article page was the insertion of internal links. For each of roughly 2,000 keys a regular expression was built and run over the entire text. That is 35 ms of CPU per view, and on a shared server with a load average of 5–9 those milliseconds stretched into more than 0.1 s of response time. We replaced it with a substring search with a word-boundary check. We compared the old and new versions on 679 texts (all published articles and page intros in three languages): the output is byte-for-byte identical. The heaviest article is processed in 5.6 ms instead of 18.9, the entire set in 464 ms instead of 1,451.

An honest caveat: the “before” numbers in these cases were taken on different days and in different ways. This is not one curve but four separate stories, and adding them up into a single “made it N times faster” would be untrue.

Where things stand now and what the network has to do with it

On 16 September 2026 at around 20:40 Kyiv time we repeated the measurement. From the server itself to the site’s public address, 5 requests per URL, median time to first byte:

Page type Median
service pages, 6 URLs in three languages 0.211–0.266 s
articles, 3 URLs 0.222–0.307 s
section hub 0.216 s
“About us” 0.178 s
homepage 0.346 s

The server’s load average at the time of measurement was 6.1, so these are figures under its usual load, not on an idle machine.

Now what a person in Ukraine sees. From our PC, 6 requests each:

URL Time to first byte
static logo file 0.185–0.215 s
“About us” 0.254–0.362 s
article 0.319–0.394 s
homepage 0.328–0.433 s

The site does not render a static file at all; it simply sits on disk. And still 0.19–0.22 seconds, of which about 0.1 s goes just to establishing the connection. This is the floor: code optimization will not take you below it, because it is distance and network, not the program.

How to check your site in 10–15 minutes

1. Measure the floor. Take any static file on your site (a logo, CSS) and the pages that concern you:

for u in /logo.svg / /katalog/; do
  printf "%s ::" "$u"
  for i in 1 2 3 4 5 6; do
    curl -s -o /dev/null -w " %{time_starttransfer}" "https://your-site.com.ua$u"
  done; echo
done

Subtract the static file’s time from the page’s time. The remainder is the work of your server and your code. If the remainder is a few dozen milliseconds, there is nothing to look for in the code; the issue is the network or the server’s location. If it is a second, read on.

2. Compare different page types. Homepage, category, product page, article, “Contacts.” If they are all equally slow, even the simplest ones, the cause is almost certainly in what is on every page: the header, menu, footer, settings. If only the category is slow, look at its own queries.

3. Find the heavy query. Turn on the slow query log and open the page a few times. For MySQL:

SET GLOBAL slow_query_log = 1;
SET GLOBAL long_query_time = 0.1;

For PostgreSQL with the pg_stat_statements extension:

SELECT round(mean_exec_time) AS ms, calls, left(query, 120)
FROM pg_stat_statements ORDER BY mean_exec_time DESC LIMIT 10;

A query with a large calls value and noticeable time is the prime suspect: it runs on every view.

4. See how much the columns it fetches weigh. In our case the time went not into searching but into shuffling data. In PostgreSQL:

SELECT avg(pg_column_size(column_name)) FROM table_name;

If a menu query fetches a column of tens of kilobytes per row and displays one word from it, you have found the same thing we did.

We'll be cautious about OpenCart and similar engines here, because we did not measure this separately: there the role of our mega menu is usually played by modules in the header and footer that run on every page. They are worth checking first, but this is an analogy, not a measured fact.

What this did not fix

The most important limitation shows in real visitor data. Over the 28 days up to 7 September 2026, the 75th percentile of time to first byte on mobile was 893 ms (109 measurements), on desktop 654 ms (124 measurements). Only visitors who consented to analytics are counted, and the window partly covers the period before the 5 September fixes. But the gap between 0.2 s on the server and almost 0.9 s on a phone is mostly the mobile network, and the site’s code cannot remove it. Only serving ready-made pages from a cache, without rendering, could help. Our Ukrainian pages are still rendered on every request: their URLs have no language prefix, the path is rewritten, and a rewritten request bypasses the page cache.

Caching has its own price, too. The menu updates immediately after publishing in the admin panel, but site settings can lag by up to a minute. We accepted this deliberately.

The takeaway that applies to any site: before blaming hosting or the CMS, measure the floor with a static file, and then look for one heavy query in a block that sits on every page. If you have no time to do this yourself, we do a site speed audit and site acceleration with the same before and after measurements. Which speed numbers to measure in the first place is covered separately, and for page weight and compression there is the story of 170 KB instead of 76.

Tags

PerformanceAnalytics

🤔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

https://lionex.com.ua/blog/chomu-storinka-dumala-try-sekundy

💚 Thank you for helping us grow

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.

Frequently asked questions

Answers to common questions on the topic

Because the time goes not to the server as such, but to the work the code does on every view. In our case, neighboring projects on the same server responded in 0.2–0.3 s, while our site took 3.05 s. The cause was a single mega menu query in the header that ran on every page.

Measure time to first byte for a static file, such as a logo, and for a regular page, with several requests for each. The site does not render a static file, so its time is the network floor. The difference between the page and the file is the work of your server and code.

In the blocks that are on every page: the header, menu, footer, site settings. If even the simplest pages are equally slow, the cause is almost certainly there. After that, the database slow query log helps: the suspect is the query that runs very often and takes noticeable time.

No. A cache removes repeated work by the code, but not the network between the server and the visitor. Besides, caching has a price: on our site, settings can take up to a minute to appear after an edit in the admin panel. A cache without an invalidation mechanism is outright harmful, because the edit simply does not appear.

We did not measure this on OpenCart separately, so we are talking about an analogy, not a measured fact. There the role of our mega menu is usually played by modules in the header and footer that run on every page, so they are worth checking first.

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.