invisit
All posts

By Miraç Berk16 min read

How big is your analytics script? We measured 15 trackers

GA4, PostHog, Mixpanel, Plausible, Fathom, invisit and 9 more trackers measured raw, gzip and brotli, plus what loads later and how to check your own.

Soft 3D script files of very different sizes resting on a balance scale, lit by violet dithered light with soft shadows.

On November 3, 2026, the tracking scripts of 15 web analytics tools ranged from 1.3 KB to 179 KB gzipped. Plausible’s legacy script.js was the smallest, and Google Analytics 4’s gtag.js the largest: 155 KB for a made-up ID and up to 179 KB for a live property.

Most privacy-focused trackers sit between 2 and 12 KB. Session recording and product analytics tools run from 27 KB (Clarity) to 119 KB (PostHog), and several of them download more code later for session replay and surveys. invisit, the tool we build, loads about 8.5 KB, which puts it in the middle: seven tools in this test are lighter (Plausible, Vercel, Fathom, Pirsch, Umami, Simple Analytics and DataFast) and seven are heavier (Cloudflare, Rybbit, Matomo, Clarity, Mixpanel, PostHog and GA4).

Below are the full table, what those bytes cost a visitor, what loads later, what the smaller scripts leave out, and the commands to reproduce every number. KB means 1,000 bytes throughout.

Analytics script sizes compared (November 2026)

Across the 15 tools, the core analytics script ran from 1,285 bytes gzipped (Plausible’s legacy script.js) to 178,838 bytes (GA4’s gtag.js for a live property). The table lists the file each tool’s standard snippet loads, measured four ways, in bytes: raw (what the browser parses), gzip at level 9 and brotli at quality 11 (two references that compare files fairly), and what the vendor’s server actually sent to a client accepting the encodings Chrome accepts. Rows are sorted by gzip size.

Tool File Raw Gzip -9 Brotli 11 Sent to Chrome
Plausible (legacy) script.js 2,841 1,285 1,122 1,361 (zstd)
Vercel Web Analytics script.js 4,522 2,033 1,834 2,076 (br)
Fathom script.js 6,981 2,086 1,793 2,283 (zstd)
Plausible (per-site) pa-*.js 5,972 2,485 2,193 2,658 (zstd)
Pirsch pa.js 11,842 3,498 3,041 3,723 (zstd)
Umami script.js 8,575 3,913 3,533 4,099 (br)
Simple Analytics latest.js (v12) 9,611 4,793 4,299 5,147 (zstd)
DataFast script.js 15,376 5,250 4,618 5,456 (br)
invisit invisit.js 20,330 8,542 7,720 7,720 (br)
Cloudflare Web Analytics beacon.min.js (2026.10.1) 31,853 10,253 9,322 10,264 (gzip)
Rybbit script.js 37,351 11,753 10,599 12,118 (gzip)
Matomo, self-hosted matomo.js (5.14.2) 68,294 22,132 19,293 Your server decides
Microsoft Clarity loader + clarity.js (0.8.75) 73,982 26,669 23,507 25,870 (none + br)
Mixpanel mixpanel-2-latest.min.js (2.84.0) 103,263 33,511 29,552 33,589 (gzip)
Matomo Cloud matomo.js (public demo) 225,180 65,427 56,199 62,782 (br)
PostHog array.js (1.445.0) 369,516 118,618 99,716 118,879 (br)
Google Analytics 4 gtag.js, made-up ID 445,931 154,992 128,985 156,756 (zstd)
Google Analytics 4 gtag.js, five live properties 481,663 to 542,687 163,538 to 178,838 136,030 to 148,217 166,033 to 181,330 (zstd)

The five live GA4 properties are the ones on tailwindcss.com, docusaurus.io, eslint.org, pnpm.io and prettier.io. The Clarity row adds a live project’s 726-byte loader to clarity.js. Self-hosted Matomo comes from the 5.14.2 release on GitHub, so its transfer size depends on how your own server compresses it. Plausible replaced script.js with a per-site pa-*.js file in October 2025 (Plausible’s update guide); the old file is still served, and we measured the new one Plausible uses on its own site.

Horizontal bar chart of core tracking script sizes, gzipped: Vercel Web Analytics 2.0 KB, Fathom 2.1, Plausible 2.5, Pirsch 3.5, Umami 3.9, Simple Analytics 4.8, DataFast 5.2, invisit 8.5, Cloudflare Web Analytics 10.3, Rybbit 11.8, Matomo 22.1, Microsoft Clarity 26.7, Mixpanel 33.5, PostHog 119 and Google Analytics 4 155 KB.
The core script of each tool, gzip -9, measured November 3, 2026.

Why are GA4, PostHog and Mixpanel scripts so big?

The large analytics scripts are not page-view counters. Each carries a tag runtime, a product suite or a session recorder, and every visitor downloads it on the first page view.

What does a heavier analytics script cost your visitors?

A heavier analytics script costs a visitor three things: the bytes over the network, the work to parse and compile them, and the main-thread time to run them. Under about 12 KB all three are small. At 100 KB and up they add up on slow connections and slow phones.

Download time

Here is simple arithmetic at Lighthouse’s default mobile profile, 1.6 Mbps down with a 150 ms round trip (Lighthouse throttling docs): the bytes actually sent, times eight, divided by 1.6 Mbps.

Script Bytes sent Transfer at 1.6 Mbps
gtag.js, largest live property 181,330 0.89 s
gtag.js, made-up ID 156,756 0.77 s
PostHog array.js 118,879 0.58 s
Mixpanel 33,589 0.16 s
Cloudflare beacon.min.js 10,264 0.05 s
invisit.js 7,720 0.04 s
Plausible pa-*.js 2,658 0.01 s

Analytics snippets normally load with async or defer, so this download does not block the HTML parser. On a slow connection, those bytes still take bandwidth the rest of the page is waiting for.

Every analytics script from a third-party host also needs a connection to that host first, and setting one up “might involve up to three round trips” (web.dev). At a 150 ms round trip, three round trips are 450 ms, the same for a 1 KB file as for a 180 KB one. Serving the script from your own domain removes that setup; Plausible, Rybbit and invisit each document a proxy (Plausible, Rybbit, invisit).

Parse and compile

Chrome’s V8 engine can parse and compile a downloaded script “without blocking the main thread”, and the V8 team wrote in 2019 that “the dominant costs of processing scripts are now download and CPU execution time” (V8 blog). From the third load on, V8 can skip compiling by reusing a code cache, but only while the file keeps coming from the HTTP cache or a 304 Not Modified; a fresh 200 response “clears the code cache” (V8’s code caching guide). That ties compile cost to the cache lifetimes further down.

Main-thread execution

Running the code happens on the main thread, where any task over 50 ms is a long task that delays the next click or tap (web.dev). Interaction to Next Paint, the Core Web Vital for responsiveness, is good at 200 ms or less at the 75th percentile (web.dev), and Lighthouse flags third-party code that blocks the main thread for 250 ms or more (Chrome docs).

We did not measure CPU time. It depends on the device and on what each script does, which file size only hints at. A session recorder, for one, serializes the whole page and then records every change and interaction (rrweb’s README), work a page-view counter never does.

Said plainly: the gap between a 2 KB and a 9 KB tracker is about 35 ms of transfer at 1.6 Mbps. The gap between 9 KB and 180 KB is most of a second at the same speed, plus 446 to 543 KB of uncompressed JavaScript for the phone to compile and run.

Gzip, brotli or on the wire: which script size is the real one?

The size a visitor pays for is the on-the-wire size, the bytes the browser receives. Gzip -9 and brotli 11 are references for comparing files fairly. The three differ because the algorithms differ, and because each server picks its own encoding and compression level.

When a vendor quotes a script size, check which of these it is before you compare it with anything.

Which analytics tools load more code later?

PostHog, Mixpanel, Umami and Rybbit download a separate, often larger file for session replay, and PostHog fetches more files for surveys and other features. invisit loads small modules for web vitals, heatmaps and feedback. These files are not in the main table.

Tool File Loads when Gzip -9 Brotli 11 Sent to Chrome
PostHog lazy-recorder.js Session replay is on 68,449 59,144 69,970 (br)
PostHog surveys.js Surveys are on 47,076 41,342 47,507 (br)
PostHog product-tours.js Product tours are on 40,676 36,034 41,740 (br)
PostHog conversations.js Conversations are on 23,754 21,242 24,820 (br)
PostHog dead-clicks-autocapture.js Dead click capture is on 8,517 7,777 8,763 (br)
PostHog exception-autocapture.js Exception capture is on 6,544 5,937 6,841 (br)
PostHog web-vitals.js Web vitals are on 3,941 3,594 4,086 (br)
Mixpanel mixpanel-recorder-CcIR1Do8.min.js A session is picked for replay 104,614 62,743 104,699 (gzip)
Rybbit replay.js Session replay is on 81,659 58,013 85,398 (gzip)
Umami recorder.js You add the tag for replays or heatmaps 58,422 37,764 45,455 (br)
invisit invisit-extras.js Web vitals are on (the default) 1,116 1,019 1,019 (br)
invisit invisit-interactions.js Heatmaps are on 1,488 1,312 1,312 (br)
invisit invisit-feedback.js The feedback widget is on 3,575 3,116 3,116 (br)
invisit invisit-overlay.js You open a heatmap on your own page 8,623 7,740 7,740 (br)

posthog-js 1.445.0 asks for lazy-recorder.js unless the project’s remote config names another bundle; the older recorder.js measured 42.7 KB. Mixpanel’s code fetches its recorder only for sessions picked by record_sessions_percent (0 by default) or a recording already in progress. Umami’s recorder.js is a second script tag you add yourself (Umami’s replay docs), so every visitor downloads it, even though Umami records 15% of sessions by default. Rybbit fetches replay.js when session replay is on for the site.

With replay on, the totals change a lot. In gzip bytes: PostHog with replay and surveys is 234.1 KB, Mixpanel with its recorder 138.1 KB, Rybbit with replay 93.4 KB and Umami with its recorder 62.3 KB. invisit with web vitals, heatmaps and the feedback widget is 14.7 KB, with no session recordings.

All four replay recorders contain rrweb, the open-source recorder. Clarity is the exception, and it deserves the credit: its 26.2 KB clarity.js is the smallest file in this test that records sessions.

What does a smaller analytics script give up?

A smaller analytics script gives up features. The lightest trackers count page views, referrers, campaigns and custom events; heatmaps, session replay, web vitals and funnels are what push the size up. Match the script to the features you will use, not to the lowest number in the table.

Under 3 KB: Plausible, Vercel and Fathom.

3 to 6 KB: Pirsch, Umami, Simple Analytics and DataFast.

8 to 12 KB: invisit, Cloudflare and Rybbit.

20 KB and up: Matomo, Clarity, Mixpanel, PostHog and GA4. These carry visit logs, product analytics, recordings or an ad platform: Matomo’s visits log and Cloud heatmaps (our Matomo comparison), Clarity’s free recordings and heatmaps, Mixpanel’s autocapture and replay (our Mixpanel comparison), PostHog’s flags, experiments and surveys (our PostHog comparison), and GA4’s funnel explorations and Google Ads and BigQuery links (our GA4 comparison). If GA4 is the one you are replacing, start with Google Analytics alternatives.

How long does the browser cache each analytics script?

Analytics scripts stay in the browser cache for anything from zero seconds to 31 days, set by each vendor’s Cache-Control header. Since Chrome 86 the HTTP cache is partitioned by site, so a copy cached while visiting another site never helps yours (Chrome blog).

Cache-Control we received Scripts
31 days Vercel
8 days Matomo Cloud demo
7 days Simple Analytics
1 day Plausible script.js, Umami, Cloudflare, Pirsch, clarity.js
4 hours PostHog
1 hour invisit, Rybbit
15 minutes, private, no ETag GA4 gtag.js
10 minutes Mixpanel
60 seconds Plausible pa-*.js
0, revalidated with an ETag Fathom, DataFast
no-store Clarity loader (726 bytes, every page view)

A short lifetime with an ETag or Last-Modified date costs little: the browser asks, gets a 304 Not Modified, and V8 keeps its compiled code. GA4’s gtag.js came with neither, so after 15 minutes the next page view downloads the full 157 to 181 KB again, and that fresh response clears V8’s code cache.

These script sizes will drift

Vendors ship tracker updates every few days, so treat these numbers as a snapshot of November 3, 2026, and re-run the commands below before you quote one.

The Last-Modified headers we received show it. PostHog’s array.js and all its extra files changed at 10:52 UTC that morning, 48 minutes before we fetched array.js, and Vercel’s script at 10:43 UTC. Umami’s script and recorder, DataFast’s script and Clarity’s clarity.js changed the day before, Rybbit’s on November 1. Our own PostHog comparison, written earlier the same day, recorded posthog-js 1.444.4; we measured 1.445.0.1

How we measured the scripts

We fetched every file on November 3, 2026, between 11:35 and 11:50 UTC, from one machine with a Chrome 141 user agent.

  1. Download each file with curl --compressed and count its bytes: that is the raw size.
  2. Compress it with GNU gzip 1.14 at -9 -n (-n keeps the file name out of the header) and with brotli at quality 11 through Bun’s node:zlib, since no brotli CLI was installed.
  3. Download it again with Accept-Encoding: gzip, deflate, br, zstd, keep the bytes as sent, and read the content-encoding and cache-control headers.
  4. Take the URLs of the extra files from each main script’s own code, and fetch them the same way.
measure.sh
#!/usr/bin/env bash
# Usage: ./measure.sh <script URL>
set -euo pipefail
url=$1
ua='Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36'

# The file as the browser parses it, decompressed
curl -sSL -A "$ua" --compressed -o script.js "$url"
echo "raw:    $(wc -c < script.js)"
echo "gzip:   $(gzip -9 -n -c script.js | wc -c)"
echo "brotli: $(bun brotli.ts script.js)"

# What the server sends to a browser that accepts what Chrome accepts
curl -sSL -A "$ua" -H 'Accept-Encoding: gzip, deflate, br, zstd' -D headers.txt -o wire.bin "$url"
echo "wire:   $(wc -c < wire.bin)"
grep -i -E '^(content-encoding|cache-control|last-modified):' headers.txt
brotli.ts
import { brotliCompressSync, constants } from 'node:zlib'

const bytes = await Bun.file(process.argv[2]!).bytes()
const brotli = brotliCompressSync(bytes, { params: { [constants.BROTLI_PARAM_QUALITY]: 11 } })
console.log(brotli.byteLength)

If you have the brotli CLI, brotli -q 11 -c script.js | wc -c replaces the Bun step. These are the URLs we fetched:

Tool URL Version or date in the file or headers
Plausible (legacy) https://plausible.io/js/script.js None
Plausible (per-site) https://plausible.io/js/pa-6_srOGVV9SLMWJ1ZpUAbG.js None; plausible.io’s own site
Vercel Web Analytics https://nextjs.org/_vercel/insights/script.js 0.1.3 in the file
Fathom https://cdn.usefathom.com/script.js Last-Modified August 20, 2026
Pirsch https://api.pirsch.io/pa.js Last-Modified October 29, 2026
Umami https://cloud.umami.is/script.js and /recorder.js Last-Modified November 2, 2026
Simple Analytics https://scripts.simpleanalyticscdn.com/latest.js v12, dated 2026-10-22 in its header
DataFast https://datafa.st/js/script.js Last-Modified November 2, 2026
invisit https://api.invisit.dev/invisit.js and invisit-*.js beside it Production build of November 3, 2026
Cloudflare https://static.cloudflareinsights.com/beacon.min.js 2026.10.1 in the file
Rybbit https://app.rybbit.io/api/script.js and /api/replay.js Last-Modified November 1, 2026
Matomo https://raw.githubusercontent.com/matomo-org/matomo/5.14.2/matomo.js 5.14.2, released October 22, 2026
Matomo Cloud https://demo.matomo.cloud/matomo.js Last-Modified September 28, 2026
Microsoft Clarity https://www.clarity.ms/tag/<project id> and https://scripts.clarity.ms/0.8.75/clarity.js 0.8.75
Mixpanel https://cdn.mxpnl.com/libs/mixpanel-2-latest.min.js and mixpanel-recorder-CcIR1Do8.min.js 2.84.0
PostHog https://us-assets.i.posthog.com/static/array.js and /static/<name>.js?v=1.445.0 1.445.0
Google Analytics 4 https://www.googletagmanager.com/gtag/js?id=G-0000000000, then the IDs G-TMR96VNVMT, G-E5CR2Q1NRE, G-7DGPHY308T, G-RPD4HWL84Z and G-T57CP1SG02 No version; configuration embedded

Check the analytics script on your own site

The numbers that matter are the ones your own pages load, with your own settings. Chrome DevTools shows them in a few minutes.

  1. Open your site in Chrome, open DevTools, go to the Network panel and tick Disable cache, which emulates a first visit (DevTools network reference).
  2. In the panel settings, turn on Big request rows. The Size column then shows the transferred size on top and the uncompressed size below it.
  3. Filter by JS, reload, and find your analytics host. Note both sizes and the content-encoding response header.
  4. Use the page for a minute with your features on. Replay recorders, surveys and feedback widgets often arrive after the first page view.
  5. Run Lighthouse and read “Reduce the impact of third-party code” for the main-thread time.

Footnotes

  1. A new Last-Modified date can also mean a redeploy with unchanged code. PostHog’s version string did change. ↩