I made my analytics wait for the page. Here is what it cost
Google's analytics scripts were 60% of what a reader downloaded to open a post on this site, and they started before the post had finished arriving. I made them wait until the page is idle or the reader first touches it. The numbers, the code, the visits it may not count, and the part a speed test does not see.
Also available in 中文
What Google was costing a post page
On the morning of 2 October 2026, Google was 297 KB of every post page on this site, out of 492 to 494 KB in total.
Earlier that day I had removed the cookie banner. Analytics now loaded for every reader, with an off switch behind the Cookies link in the footer, and with analytics off by default for readers in the European Economic Area, the UK and Switzerland. The banner had been holding Google back until a reader clicked Allow. Without it, two scripts started on every page as soon as it began to load: gtm.js, the Tag Manager container, about 122 KB, and gtag/js, the Google Analytics tag, about 174 KB.
I measured the mamak table post with Lighthouse in its mobile setting, two runs per page. The English page scored 94 and 75. The Chinese page scored 95 and 92. The home page scored 68 and 56.
The words a reader came for were a small part of what the phone was downloading.
Why the order matters, not only the size
A script marked async does not block the page from being drawn, and it still competes with it. On a phone the Google files share one connection with the post's own images and fonts, and when they arrive they run on the same main thread that is trying to lay out the text.
Analytics does not need to be first. It needs to arrive once, and to arrive in time to record the visit.
So the question was not whether to keep Google. It was when to ask for it.
The change: wait for idle or the first touch
The rule since 2 October is simple. The page loads. Then the first of two things requests gtm.js: the browser saying it is idle, or the reader doing something.
In detail:
- The consent defaults, and a reader's stored choice, are pushed straight away, as before. Nothing is ever sent under the wrong consent.
- After the page's
loadevent, the script asks the browser to call it when it is idle, with a 2,000 ms limit. Safari has no idle callback, so there it waits 1,500 ms afterload. - If the reader scrolls, taps, clicks, uses the wheel or presses a key first, gtm.js is requested at that moment.
- Whichever comes first wins and cancels the others. It happens once per visit, also when the reader moves to another page on the site.
- A reader who has turned analytics off requests nothing from Google at all.
This is the core of it, shortened from the script that runs on this site:
const INTERACTIONS = ['pointerdown', 'keydown', 'touchstart', 'scroll', 'wheel'];
const LISTEN = { capture: true, passive: true };
let loaded = false;
function loadGtm() {
if (loaded) return;
loaded = true;
INTERACTIONS.forEach((type) => window.removeEventListener(type, loadGtm, LISTEN));
window.dataLayer.push({ 'gtm.start': Date.now(), event: 'gtm.js' });
const s = document.createElement('script');
s.async = true;
s.src = 'https://www.googletagmanager.com/gtm.js?id=GTM-XXXXXXX';
document.head.appendChild(s);
}
function waitForIdle() {
if ('requestIdleCallback' in window) requestIdleCallback(loadGtm, { timeout: 2000 });
else setTimeout(loadGtm, 1500);
}
INTERACTIONS.forEach((type) => window.addEventListener(type, loadGtm, LISTEN));
if (document.readyState === 'complete') waitForIdle();
else window.addEventListener('load', waitForIdle, { once: true });
Two details matter. The gtm.start line is pushed in the same function that adds the script, so Tag Manager's own clock starts when the file is actually requested. And the listeners are passive, so they never delay a scroll.
I work with two AI sessions, a Brain that plans and a Hand that builds. The Brain proposed this after removing the Chinese web font left Google as the heaviest thing on a post page. The Hand built it with tests for each case, including turning analytics off before Google has loaded and on again on the same page. I agreed to it only after I had read what it would cost me, which is further down.
Before and after
These are the medians from Lighthouse mobile on the live site, two runs before the change and three after, on the same day. With two runs, the "median" before is the average of the two.
| Before | After | |
|---|---|---|
| English post, score | 84.5 | 98 |
| English post, Largest Contentful Paint | 3.95 s | 1.6 s |
| English post, Total Blocking Time | 125 ms | 50 ms |
| Chinese post, score | 93.5 | 97 |
| Chinese post, Largest Contentful Paint | 2.45 s | 2.1 s |
| Chinese post, Total Blocking Time | 165 ms | 60 ms |
| Home page, score | 62 | 68 |
| Home page, Largest Contentful Paint | 5.65 s | 5.7 s |
Read the English row with care. Its "before" is the average of 94 and 75, and the 75 was one slow run. The honest summary is this: before, the post pages scored in the 90s with bad runs mixed in; after, they score 97 to 99 on most runs, and the time the page spends blocked has roughly halved or better.
The home page barely moved, and I know why. It runs a 3D scene, which costs more than analytics does, and I wrote about that cost separately.
In a real browser on a fresh visit, the page finished loading at 692 ms, gtm.js was requested at 718 ms, and the page view reached Google at 1,222 ms. With the old code the same morning, gtm.js was requested at 659 ms, before the page had loaded, and the page view went out at 1,212 ms. On a fast connection the page view leaves at about the same moment. What moved is that Google no longer starts while the page is still arriving.
The part a speed test does not see
Lighthouse ends its measurement when the network goes quiet. If that happens before the idle moment, Google never appears in the test.
That happened in two of my nine runs after the change. One English run weighed 195 KB with nothing from Google in it. Another had only gtm.js, 122 KB. A real reader still downloads all 297 KB. The files just arrive after the page.
So part of the better lab score is the lab not staying long enough. That is not a reason to undo the change: the reader's page really does finish first now. It is a reason not to quote the best run, and to trust the medians and the browser timings above more than any single score.
One more run shows why one run means little. The first home page run after the change scored 45, with the first text appearing after 22.9 seconds. Nothing on the page had changed. The network had a bad minute.
What it costs
The cost is a short visit. A reader who opens a page and leaves within a second or two, without scrolling, tapping or pressing a key, may leave before Google is asked for, and that visit is not counted.
On a fast connection the window is small: the idle moment came about 30 ms after the page loaded. On a slow phone it is longer. I do not know how many visits I lose this way, and I cannot find out, because analytics cannot count a visit it never saw. I accepted that. This site is written to be read, and someone who leaves in one second without moving has not read anything.
What it does not cost:
- Events pushed before Google arrives wait in the queue and are read when it does.
- Moving to another page on the site loads nothing twice. The second page's view is still recorded.
- A reader who turned analytics off still sends nothing. I checked that after the change with a reload, a scroll, a click and a key: zero requests to Google.
If you run ads and count conversions, your answer may be different from mine. A visit you paid for that leaves in a second is still a visit you paid for.
How to do it on your own site
You can try this in an afternoon, and measure it in the same afternoon.
- Measure first. Run Lighthouse in its mobile setting three times on a post page and note how many KB come from googletagmanager.com and google-analytics.com.
- Remove the Tag Manager snippet from the page head, but keep the
dataLayerand your consent defaults early in the page. - Add a trigger like the one above in your own script file, not an inline snippet, if your security policy forbids inline scripts, as this site's does.
- Check in a real browser that the page view still arrives: open DevTools, Network, filter for
collect, and try a fresh visit without touching the page, then one with a scroll. - Check that turning analytics off still sends nothing.
- Measure again, three runs, and compare medians, not your best run.
- Decide whether the short visits you may lose matter for your kind of site.
FAQ
Does delaying Google Tag Manager improve page speed?
It usually improves the numbers that measure how fast the page itself appears. On this site the median Largest Contentful Paint of an English post on mobile went from 3.95 s to 1.6 s, and the time the page was blocked fell from 125 ms to 50 ms. The Google files still download, after the page instead of with it.
Will I lose data if I delay Google Analytics?
You may lose very short visits: a reader who leaves within a second or two without scrolling, tapping or pressing a key. Visits where the reader stays or touches the page are still counted, and events pushed earlier wait in the queue. You cannot measure how many visits you lose, because analytics never sees them.
Should I wait for the browser to be idle or for the first interaction?
Use both and take whichever comes first. Waiting only for an interaction loses every reader who reads without scrolling. Waiting only for idle can be late on a busy phone. This site uses an idle callback with a 2,000 ms limit after the page loads, a 1,500 ms timer where the browser has no idle callback, and the first scroll, tap, click, wheel or key press.
Why does Lighthouse sometimes show no Google Tag Manager after deferring it?
Because Lighthouse stops measuring when the network goes quiet, which can happen before the idle moment. In two of nine runs on this site, the test ended before Google loaded. Real readers still download the files. Compare medians of several runs and check real timings in a browser rather than trusting one score.