# GA4 and Tag Manager: what they cost your page and how to load them

> See what GA4 and Google Tag Manager cost your page in bytes and main-thread time, the loading options ranked by cost, Consent Mode v2 defaults in code, and a container clean-up list.

Updated 2026-09-25 · Best practices · HTML version: https://getreport.app/guides/ga4-and-tag-manager-the-cost-and-the-consent

On many sites the heaviest third party is Google: `gtag.js` for Analytics 4, or the Tag Manager container and everything inside it. It is also the code most likely to run before the cookie banner is answered. This guide shows what the default snippet costs, the ways of loading it ranked from cheapest to most expensive, the Consent Mode defaults that keep it legal, and how to measure the difference in getReport. Plan an hour for the measurement and the consent setup, longer for a container clean-up.

## Quick answer

- Measure first: the [speed test](https://getreport.app/tools/speed-test) lists Google Tag Manager and Google Analytics by name in the third-party finding, with their bytes and main-thread blocking time.
- One property, one tag? Load `gtag.js` directly with `async` and skip Tag Manager. The container is only worth its weight when it manages several vendors.
- Set Consent Mode v2 defaults to `denied` before the container loads, then update them from the banner. Analytics keeps working in aggregate after "Reject".
- Fire tags on DOM Ready or later, not on Page View in `<head>`; pause every tag nobody looks at.
- Confirm with the [cookie scanner](https://getreport.app/tools/cookie-scanner): no tracker request before consent, Consent Mode detected.

## Why GA4 and Tag Manager cost more than they look

A tag manager is a promise to marketing: add a tag without asking a developer. Every visitor pays for it on every page. The container is a single file that grows with each tag, trigger and variable; the tags then load their own libraries from their own domains. Nothing in the Tag Manager interface shows this cost, so containers grow for years.

**GA4 alone.** The snippet loads `gtag.js` from `www.googletagmanager.com`, which then sends hits to `www.google-analytics.com` or a regional collection host. Expect the library to be in the region of 100 KB compressed, one or two extra origins to connect to (DNS, TCP and TLS on a phone: 100–300 ms each before the first byte) and, in Lighthouse's throttled run, tens of milliseconds of main-thread work, more once enhanced measurement attaches its listeners.

**Tag Manager.** A nearly empty container starts at a similar size and grows with every tag and every line of code in custom HTML tags. Each tag then loads its vendor: the GA4 tag loads `gtag.js` anyway, a Meta pixel loads `fbevents.js`, a chat tag loads a widget. Two origins become five or ten, and the blocking time is the sum. A container with a dozen active tags commonly pushes the third-party finding past its 250 ms line on its own. These are typical ranges, not promises; the finding gives you the real numbers for your page.

The cost lands on the main thread of a phone. While the container evaluates and its tags run, the browser cannot respond to a tap or lay out the rest of the page, which is how a tag manager ends up hurting [Interaction to Next Paint](https://getreport.app/learn/interaction-to-next-paint).

The second cost is legal. Under the ePrivacy rules and GDPR, analytics and advertising identifiers need consent before they are set. A container that fires GA4 on Page View sets `_ga` before anyone sees the banner, and the banner then records a choice nobody honoured.

## How getReport checks it

> **Free tool:** [Website speed test](https://getreport.app/tools/speed-test): Lighthouse lab results and real-user Core Web Vitals for any page, mobile and desktop, with a filmstrip, a request waterfall and a fix for every slow part. Free, no signup.

The speed test runs Lighthouse through Google PageSpeed Insights on a throttled phone profile and reads the third-party summary from it: every external origin, grouped by company, with the bytes it transferred and the time it kept the main thread busy. The finding turns to a warning above 250 KB or 250 ms in total and lists the heaviest companies first, so Google Tag Manager and Google Analytics appear by name with their own numbers.

> **Check: Third-party code weight.** Tags, widgets and embeds from other companies run on your visitors' phones at your page's expense. Over 250 KB or 250 ms of blocking is a sign they dominate the load.
>
> 1. List what each third party gives you; remove the ones nobody looks at.
> 2. Load the rest after interaction or with a facade (a static thumbnail for YouTube, a click-to-load chat button).
> 3. Move tags into a tag manager that fires them late, not in <head>.

The best-practices module adds an information line from the rendered page. It looks for the GA4 library (`googletagmanager.com/gtag/js?id=G-…` or an inline `gtag('config', 'G-…')`), the Tag Manager loader (`gtm.js` or the `gtm.start` snippet) and ten other analytics and pixel vendors, from Universal Analytics and the Meta pixel to Plausible and Matomo, and prints what it found. It carries no weight; it exists so you can compare the list with what you think is installed.

> **Check: Analytics and tag managers.** Knowing which analytics and tag manager run on the page helps you check they load only after consent and that no old trackers are left behind.
>
> 1. Remove trackers you no longer use; each one adds requests and privacy obligations.

The [cookie scanner](https://getreport.app/tools/cookie-scanner) loads the page in a fresh Chromium session with no cookies, records every request, matches them against 25 known analytics, advertising and session-replay trackers, then clicks the banner's accept button and records what changed.

![The cookie scanner's Trackers section on a page whose tags fire on load: Google Analytics 4 (gtag) and a Meta Pixel both tagged "before consent", with "2 before consent, 0 after accept" in the corner](https://getreport.app/guides/img/ga4-and-tag-manager-the-cost-and-the-consent/trackers.webp "Every tracker that sent a request before the banner was answered is listed with its kind, request count and the first URL it called.")

> **Check: No trackers load before consent.** Analytics, ad pixels and session-recording scripts that run before the visitor answers the cookie banner set identifiers without permission. Under GDPR and ePrivacy that needs prior consent; regulators fine for it, and the banner is worthless if the tags do not wait for it.
>
> 1. Load tracking tags through your consent platform (Cookiebot, OneTrust, Complianz, CookieYes, Google Tag Manager with consent triggers) so they run only after "Accept". Test in a fresh incognito window: no tracker request until you click.
> 2. If you use Google tags, add Consent Mode v2 with the defaults set to denied; the tags then send cookieless pings until consent.

That check has one deliberate exception. When the page has called `gtag('consent', …)` and no Google analytics or advertising cookie exists before the click, Google's requests are not counted: that is Consent Mode sending cookieless pings, as documented. The finding says so in its evidence line. The Consent Mode finding itself is informational and appears only when analytics were detected at all.

> **Check: Analytics run with Google Consent Mode.** Consent Mode tells Google tags what the visitor agreed to, so analytics keep working (in aggregate) after a "Reject" click instead of going dark. Without it, EU sites lose a large share of their measurement.
>
> 1. Enable Consent Mode v2 in your consent banner's settings and connect it to Google Tag Manager or the GA4 tag.

## Step by step

### 1. Record the baseline

Run the speed test and keep the report link. Note three numbers: the third-party finding's total bytes and blocking time, the long-tasks finding, and Total Blocking Time. Open the third-party finding's technical detail; the list of companies in order of blocking time is your audit. Then run the cookie scanner and note which trackers fire before consent.

### 2. Pick the cheapest loading option that fits

Ranked from the one that removes the most cost to the one that removes the least:

**Server-side tagging with a first-party endpoint.** The browser loads one small script from your own hostname (`metrics.example.com`) and sends events to it; a tagging server you run forwards them to GA4, Ads and whichever vendors you keep. The visitor's browser never talks to Google's domains, so the extra origins, the vendor libraries and most of the blocking time disappear. The cost is real: a tagging server on Cloud Run, App Engine or your own container, a monthly bill, updates, and rebuilding each tag server-side. Right for a shop with many vendors and a developer; overkill for a blog with one property.

**Tag Manager loaded only after consent, by the consent platform.** Most consent platforms (Cookiebot, Complianz, CookieYes, OneTrust) can hold the container until the visitor accepts. That is the strictest option: no request to Google before the click, and nothing to measure until then either, so every visitor who ignores the banner is invisible.

**`gtag.js` directly, without Tag Manager.** If Analytics is the only tag, the container is pure overhead. The direct snippet is one library and one connection; step 3 shows it with the consent defaults in front.

**Delayed until idle or interaction.** Cache plugins (WP Rocket's "Delay JavaScript execution", LiteSpeed Cache's delayed JS loading) or a few lines of your own code hold the container until the browser is idle or the visitor scrolls or taps. The load gets cheaper because nothing runs before the page is interactive. The trade-off is measurement: a visitor who reads for ten seconds and leaves without touching the screen may never fire the page view. Use idle with a short timeout rather than interaction only, and compare a week of numbers before and after:

```js
// In <head>, instead of the standard GTM snippet.
// Loads the container when the browser is idle, or after 3 s at the latest.
window.dataLayer = window.dataLayer || [];
dataLayer.push({ 'gtm.start': Date.now(), event: 'gtm.js' });
function loadGtm() {
  if (window.__gtmLoaded) return;
  window.__gtmLoaded = true;
  const s = document.createElement('script');
  s.async = true;
  s.src = 'https://www.googletagmanager.com/gtm.js?id=GTM-XXXXXXX';
  document.head.appendChild(s);
}
if ('requestIdleCallback' in window) {
  requestIdleCallback(loadGtm, { timeout: 3000 });
} else {
  setTimeout(loadGtm, 3000);
}
```

**Partytown.** It runs third-party scripts in a Web Worker and proxies their access to the page, so the main thread stays free. It works with GTM and gtag on many sites, and it is the most fragile option: some tags need a reverse proxy, debugging happens in a worker, and a vendor update can break the proxying. Treat it as an engineering project with a test plan, not a plugin toggle.

### 3. Set Consent Mode defaults before anything loads

Whichever option you pick, the consent defaults go first in `<head>`, before `gtag.js` or the GTM snippet. With a single GA4 tag:

```html
<!-- In <head>: consent defaults first, then the library. -->
<script>
  window.dataLayer = window.dataLayer || [];
  function gtag(){ dataLayer.push(arguments); }
  gtag('consent', 'default', {
    ad_storage: 'denied',
    ad_user_data: 'denied',
    ad_personalization: 'denied',
    analytics_storage: 'denied',
    wait_for_update: 500
  });
</script>
<script async src="https://www.googletagmanager.com/gtag/js?id=G-XXXXXXXXXX"></script>
<script>
  gtag('js', new Date());
  gtag('config', 'G-XXXXXXXXXX');
</script>
```

With Tag Manager, keep the same first `<script>` block and put the GTM snippet after it. When the visitor accepts, the consent platform (or your banner's accept handler) sends the update:

```js
// Called by your banner's accept handler.
gtag('consent', 'update', {
  analytics_storage: 'granted',
  ad_storage: 'granted',
  ad_user_data: 'granted',
  ad_personalization: 'granted'
});
```

Until the update arrives, GA4 sends cookieless pings and Google models the gaps; that is the state the cookie scanner recognises as compliant.

> **Note:**
>
> Consent Mode is a Google mechanism for Google tags. A Meta pixel or a Hotjar script does not read `gtag('consent')`; each of those still needs its own trigger tied to the banner, or the consent platform's blocking mode.
>

### 4. Clean the container

Whatever loads the container, the container decides most of the cost. Once a quarter:

1. **Pause what nobody reads.** Ask who looks at each tag's data. Pause it for a month; if nobody complains, delete it.
2. **One tag per vendor.** Two GA4 configuration tags or two Meta pixels double the requests and skew the data.
3. **No custom HTML tags that load libraries.** A custom HTML tag that injects jQuery or a large SDK runs on every page it fires on. Use the vendor's template, or fire it only where it is used.
4. **Trigger on DOM Ready or Window Loaded, not Page View,** unless the tag must be first. Page views survive a few hundred milliseconds of delay; the visitor's first paint does not.
5. **Watch the container size.** Tag Manager caps a web container at 200 KB and warns in the workspace as it gets close; long before that, the file costs you on every page.
6. **Use workspaces and versions.** Publish named versions with a note, so a speed regression can be matched to a change and rolled back in a click.

### 5. Settle the privacy settings

GA4 does not log or store IP addresses, so the old `anonymizeIp` flag from Universal Analytics has nothing to replace. What you do control is event data retention (Admin → Data collection and modification → Data retention), which defaults to 2 months and can be raised to 14 on standard properties; set it deliberately and write it into the privacy policy. Whether sending analytics data to Google is lawful for an EU site has been contested by several data protection authorities and depends on the current EU–US transfer framework, which is why some sites moved to cookieless, self-hosted analytics such as Plausible or Matomo. getReport itself uses a self-hosted Plausible instance that sets no cookies, which is why its own pages have no banner. The banner rules themselves are in [Cookies before consent](https://getreport.app/guides/cookies-before-consent).

### 6. Measure again

Run the speed test and compare the same three numbers from step 1. Look at the Lighthouse score last: a change that saves 300 ms of blocking can move it by one point or by ten, depending on what else the page does. [Long tasks and main-thread work](https://getreport.app/guides/long-tasks-and-main-thread-work) explains how to read the task list. Then run the cookie scanner again.

## Platform notes

### WordPress

Three ways to install, in order of control:

- **Site Kit** adds the GA4 snippet (or a Tag Manager container) with no theme edits and is the easiest to remove cleanly later. It does not delay anything; pair it with a consent plugin that supports Consent Mode.
- **GTM4WP** (Google Tag Manager for WordPress) places the container and pushes useful `dataLayer` values (post type, author, WooCommerce events), which removes the need for tags that scrape the page. It has a setting to move the container code out of the head.
- **A snippet in the theme** (`header.php` or a code snippets plugin) is the fastest to add and the easiest to forget; it disappears on a theme change and is invisible to the next person.

Consent plugins with a Consent Mode v2 integration (Complianz, CookieYes, Cookiebot, Real Cookie Banner) set the defaults before the container and send the update after the click. Turn that integration on rather than writing the `gtag('consent')` calls by hand, so a plugin update does not leave two competing implementations.

### Shopify

The Google & YouTube app installs GA4 and follows the visitor's choice from Shopify's own privacy banner; a hand-pasted snippet in `theme.liquid` runs alongside it and double-counts. Use one or the other. Custom pixels live under Settings → Customer events, where each pixel's permission setting decides whether it waits for consent.

### Static sites and custom code

Put the consent defaults and the loader in one partial that every layout includes, so the order (defaults, then library) is guaranteed. If the site has no banner because it uses cookieless analytics, say so on the privacy page and leave Consent Mode out; the scanner then simply finds no trackers.

## Verify

- The speed test's third-party finding lists only the vendors you kept, under 250 KB and 250 ms, and the long-tasks finding no longer names `gtm.js` or `gtag/js` among the longest tasks.
- The cookie scanner shows no tracker before consent (or only Google in Consent Mode without cookies), and Consent Mode detected.
- In a fresh incognito window, DevTools → Application → Cookies shows no `_ga` cookie until you click "Accept".
- GA4's real-time report still shows visits after the change; a sudden drop means the loader or the consent update broke.

## Common mistakes

- **Container in `<head>`, tags on Page View, consent handled "by the banner".** The banner records a preference; the tags never read it. Symptom: `_ga` set before the click in the cookie scanner. Fix: Consent Mode defaults, or blocking mode in the consent platform.
- **Two GA4 properties.** One from the theme, one from Site Kit, one from the container. Symptom: two `G-` ids in the page source and doubled sessions. Keep one.
- **A Tag Manager container inside another container.** An agency adds theirs through a custom HTML tag in yours. Symptom: two `gtm.js` requests in the waterfall. Merge them into one container with folders.
- **Tags that load jQuery.** A custom HTML tag pulls a 90 KB library for one selector. Rewrite it in plain JavaScript or use the vendor template.
- **Judging only by the Lighthouse score.** Loading the container on idle can leave the score almost unchanged while cutting 400 ms of blocking. The third-party and long-tasks findings show it; measure the parts, not the total.
