Dev.to WebDev 🛠 Dev 👁 0 📖 8 min read

Your analytics tag is slowing your site down

Originally published on kunjdeveloper.com. One Google Analytics tag, pasted into the <head> the way the setup guide says, cost a plain article page 10 Lighthouse points and 424 ms of blocking time on mobile. I measured

Your analytics tag is slowing your site down

Originally published on kunjdeveloper.com.

One Google Analytics tag, pasted into the <head> the way the setup guide says, cost a plain article page 10 Lighthouse points and 424 ms of blocking time on mobile. I measured it, five runs each. The numbers are below.

I went looking because of an evening spent running Lighthouse on sites I like. Plenty of them scored badly on mobile, and the reports kept pointing at the same thing. Not their code. Their tracking. On most of them the tag scripts alone came to roughly 600 KB, give or take: the Meta Pixel, GA4, Microsoft Clarity and a handful of bot-detection scripts, all of it loaded and run before anything else got a turn.

I won't name them. I don't own them, and I don't know what their teams are up against. But I made the same mistake on my own site, so that one's fair game.

Nobody owns the cost

Here's how it usually goes. An agency or the SEO team sends over a Google Tag Manager snippet. The developer pastes it into the <head>, because that's what the instructions say. Then, over a year or so, tags pile up inside the container: GA4, an ads pixel, a heatmap, a chat widget. Each one made sense to someone when it went in.

Two things kept showing up.

Universal Analytics, still loading in 2026. Google stopped processing its data in July 2023. Nobody told the developer it could go, so it didn't.

And GA4, twice. Once as a tag inside GTM, once as a snippet someone handed the developer to paste into the code. Two downloads of the same script, two hits for every visit.

Neither of those is a coding mistake. Marketing owns the tags, the developer owns the page, and what the tags cost the page falls in the gap between them.

Diagram: the SEO team or agency adds tags to the GTM container while the developer pastes a hardcoded GA4 snippet. Inside the container, Universal Analytics is dead and should be deleted, GA4 duplicates the hardcoded one, a session recorder should be sampled, and a chat widget firing on all pages should be narrowed. Every page downloads and runs all of it.

My own site did it too

When I measured my site on Lighthouse's mobile profile, gtag.js was about 176 KB and ran for over a second right after the page hydrated. Over a second where a tap on a button just sat there, on every visit, so I could count the visit.

I moved it to load later (code further down). That change shipped alongside a pile of other startup fixes, though, so I can't pull a clean GA-only before and after out of it. So I built a test page.

The test

A plain article page with no JavaScript of its own, in three versions: no analytics, GA4's standard snippet in the <head>, and GA4 deferred with the snippet I'll show you further down. Lighthouse 13.6, mobile profile, five runs of each, alternating between versions.

I used a placeholder measurement ID. A real one ships your property's configuration inside the file: for my real ID it's 545 KB unzipped, against 446 KB for the placeholder. So if anything, these numbers flatter GA.

With no analytics, the page scored 100 with 0 ms of blocking time, every run. The deferred version did exactly the same: 100 and 0 ms, every run.

The standard snippet scored between 88 and 91, median 90. Blocking time ran from 391 to 469 ms, median 424. gtag.js came over the wire at 157 KB (446 KB unzipped) and ran for about 540 ms on Lighthouse's simulated phone.

Ten points. From one tag, on a page that otherwise blocks for nothing at all.

Bar chart of Total Blocking Time on Lighthouse mobile: no analytics 0 ms with a score of 100, GA4 in the head 424 ms median (runs from 391 to 469 ms) with a score of 90, GA4 deferred 0 ms with a score of 100.

That deferred 0 ms needs a footnote. The script didn't get faster. It still runs, four seconds after load or on the first scroll or tap, and by then Lighthouse has stopped measuring. Same work, moved out of the moment the page is loading and someone is trying to use it. I also checked nothing got lost: the page view arrived, and an event I fired before GA had even loaded went out with the next batch once it did.

Timeline diagram: with GA4 in the head, gtag.js runs for about 540 ms right after first paint and a tap during it has to wait. With GA4 deferred, a tap after first paint is answered straight away, and gtag.js runs later, outside the window Lighthouse measures.

"But the snippet is async"

It is. That fixes half the problem.

async means the browser keeps reading your HTML while the script downloads. It says nothing about what happens once the script runs, and it runs on the main thread, the same thread that handles taps, clicks and typing. Then GTM loads every tag in the container, and those run on the main thread too.

Lighthouse calls this Total Blocking Time: every task over 50 ms between first paint and the page being reliably interactive counts against you. It carries 30% of the performance score, more than any other metric.

A small correction to how this usually gets told: TBT isn't a Core Web Vital. It's a lab number. The Core Web Vitals Google measures on real visitors are LCP, CLS and INP, and INP (how fast the page responds to input) is the one tags hurt, for the same reason. A tap that lands while the main thread is busy with tags waits. Bad TBT in the lab tends to show up later as bad INP in Search Console.

Measure first

Before you change anything, get two numbers: how slow the page is, and how much of that is tags.

  1. Run Lighthouse in Chrome DevTools on the mobile profile and open Reduce the impact of third-party code. It lists each third party with its transfer size and how long it held the main thread.
  2. In the Network panel, right-click a googletagmanager.com request and choose Block request domain. Do the same for the other tag domains, then run Lighthouse again.
  3. Run each version at least five times. Scores swing by several points between runs of the same page, so one run of each tells you nothing.

The gap is what your tags cost. "The developer wants to remove tracking" goes nowhere in a meeting. "Our tags cost us X points on mobile" gets an answer.

Clean out the container

This is the cheapest fix there is, and usually the biggest. Sit down with whoever owns marketing and go through the container one tag at a time.

Start with Universal Analytics. It sends data nowhere now, so delete it, along with tags for campaigns that ended and pixels for tools nobody pays for anymore. Then look for GA4 twice: search the codebase for gtag/js and check the container for a GA4 tag with the same measurement ID. Keep one. I'd keep the GTM one, but either works, as long as it's one.

Next, triggers. A chat widget or booking tool that matters on two pages has no business firing on "All Pages". And go easy on session recorders and heatmaps. They watch everything on the page, which makes them some of the heaviest scripts you can add. Sample them, or run them for a few weeks while you're actually researching something, then switch them off.

Every tag that survives gets an owner and a reason. A tag with no owner never gets removed.

Load what's left later

Analytics needs to know the visit happened. It doesn't need to know in the first 500 ms.

On my site, GA loads on the visitor's first interaction (a tap, a key press, a scroll) or a few seconds after the page finishes loading, whichever comes first. The timer is for people who read without touching anything. They still count.

Here's the plain HTML version. It works on any site:

<script>
  window.dataLayer = window.dataLayer || [];
  function gtag() { dataLayer.push(arguments); }
  gtag('js', new Date());
  gtag('config', 'G-XXXXXXXXXX');

  let loaded = false;
  function loadAnalytics() {
    if (loaded) return;
    loaded = true;
    const s = document.createElement('script');
    s.async = true;
    s.src = 'https://www.googletagmanager.com/gtag/js?id=G-XXXXXXXXXX';
    document.head.appendChild(s);
  }

  ['pointerdown', 'keydown', 'scroll', 'touchstart'].forEach((type) =>
    addEventListener(type, loadAnalytics, { once: true, passive: true })
  );
  addEventListener('load', () => setTimeout(loadAnalytics, 4000));
</script>

The inline part is safe to run early. gtag() only pushes onto the dataLayer array, and the real script works through that queue when it arrives, so events sent before it loads wait instead of disappearing. For GTM, swap the src for your gtm.js URL and keep the dataLayer setup.

It isn't free. Someone who lands and leaves within those few seconds without touching the page won't be counted. On most sites that's a thin slice of bounces, but agree on it with marketing before you ship, not after they spot the dip. And when a tap is what loads GA, the script runs right after that tap, so the next tap can feel slower. Still far better than running it during page load, and it's why the container clean-out matters even when you defer.

On Next.js, @next/third-parties ships GoogleAnalytics and GoogleTagManager components that load after hydration, which already beats a raw tag in the head, and next/script with strategy="lazyOnload" waits for the browser to go idle. I went a step further and only mount the GA component after the first interaction or the timer, because even after hydration it was one long task on a phone.

If it's still heavy

There are two bigger options, both with strings attached.

Partytown moves third-party scripts into a web worker, off the main thread. Some tags are happy there. Others break, especially ones that need to touch the page synchronously, so test every tag. On Next.js, the worker script strategy that uses it is still experimental and doesn't work in the App Router.

Server-side GTM has the browser send one stream of events to a server you run, which forwards them to GA4, the ad platforms and the rest. One script in the visitor's browser instead of one per vendor. You pay to host it and someone has to look after it, so it earns its keep once you have several vendors, not for a single GA4 tag.

Keeping it fixed

All of this rots the first time someone adds a tag without checking. Agree on a budget with marketing, something like "tags get at most 50 ms of blocking time on mobile", and check it whenever the container changes. Watch the Core Web Vitals report in Search Console too: Lighthouse is one simulated phone, Search Console is your actual visitors. If INP slips from green to orange after a release, open the container's version history before you start blaming your own code.

📰 Read the original article on Dev.to WebDev

Originally published by Dev.to WebDev. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.