Measuring real-user performance
How Tracely's performance dashboard works, from the Web Vitals the tracker collects to path-level hourly rollups and third-party timing aggregation.
Lab tools like Lighthouse answer "how fast could this page be on a simulated device?" Field data answers the question you actually care about: how fast is it for the people visiting right now, on their networks, on their hardware. Tracely's performance dashboard is built entirely on field data, and this post walks through how it works end to end: what the tracker collects, how it gets rolled up, and how the third-party timing report figures out which external scripts are slowing you down.
Everything here is behind toggles under Performance & quality in Site settings — none of it runs unless you turn it on.
What the tracker collects
With Performance & Web Vitals enabled, the tracker reports a timing sample per page load. The dashboard summarizes these as top cards with green/amber/red thresholds:
- TTFB — time to first byte.
- FCP — first contentful paint.
- LCP — largest contentful paint.
- DOM Load and Page Load — the classic document milestones, in milliseconds.
- CLS — cumulative layout shift.
- INP — interaction to next paint, stored as
inp_ms.
Alongside the vitals, each sample carries context that turns out to be more useful than I expected:
- Navigation type — whether the load was a navigate, reload, or back/forward restore. Reloads and bfcache restores have very different timing profiles, and mixing them into one average hides regressions.
- Redirect chains —
redirect_countandredirect_time_msstraight from the Navigation Timing API. No extra markup needed; if your marketing URLs bounce through two redirects before landing, this is where it shows up as real user-visible milliseconds. - Document cache heuristic — using
nav_transfer_sizeagainstdecodedBodySizefrom Navigation Timing to estimate whether the document itself came from cache.
A necessary caveat that the dashboard docs also make: these are field aggregates, biased toward users who complete loads and browsers that support each API. CLS and INP in particular don't exist in every browser, so those cards can show dashes on thin traffic. Sampling reduces volume further on busy sites. The right way to use these numbers is comparative — the same site, the same date-range length, before and after a deploy — not as absolute scores.
Path-level hourly rollups
The overall cards are cheap to compute, but the question that actually finds regressions is "which pages got slow?" — and answering that from raw samples means scanning every performance row in the period, grouped by path. That works until it doesn't.
So ingest maintains a rollup table, performance_metric_path_hourly_stats, keyed by site, hour bucket, and path. Every incoming sample upserts one row, incrementing:
samplespage_load_sumandttfb_sumlcp_sumandlcp_n— LCP summed separately with its own counter, because not every sample has an LCP value and dividing by the wrong denominator quietly corrupts the averageredirect_hit_samples,redirect_count_sum,redirect_time_ms_sum, andredirect_max— how many loads hit a redirect at all, the totals, and the worst chain seen in that hour
The rollup stores sums and counts, never averages. Averages don't compose — you can't merge two hourly averages without knowing their weights — but sums and counts merge trivially across any range of hours, paths, or sites. Query time becomes "add up some pre-aggregated integers and divide once," which stays fast regardless of how many raw samples sit underneath. This is the same design rule every rollup in Tracely follows, and it also means the rollup is disposable: it can always be rebuilt from raw samples.
On the dashboard this powers the per-path tables — worst LCP paths, slowest TTFB paths, redirect-heavy paths — each of which opens a detail panel with recent raw samples so you can see whether a bad average is one outlier or a real pattern. The same drill-down exists for the other segmentations: click a navigation type to see the samples behind it, or a document-cache segment to check whether "slow" is really "uncached." The redirect detail panel is the one I end up in most — it lists the recent samples for a path with their redirect counts and times, which is usually enough to spot the http-to-https-to-www chain some campaign link has been dragging around.
One design note on why the rollup is keyed by hour rather than by day: hourly buckets are the smallest unit that still merges cheaply into every period the dashboard offers — a few hours of today, last 7 days, an arbitrary custom range — without ever splitting a bucket. Daily buckets would be smaller and faster still, but they can't serve an intraday window, and "did the deploy from this morning make things worse" is exactly the query this table exists for.
Third-party timing: which scripts are slowing you down
My favorite part of the performance dashboard, because it answers the question nobody's lab tooling answers well: of all the external scripts, fonts, and pixels on your pages, which ones are actually costing your visitors time?
With the third-party timing toggle on, the tracker waits until after load, reads the Resource Timing entries, and sends a single $third_party_timing event summarizing the slowest cross-origin resources. The payload is deliberately compact:
{ "items": [ { "host": "cdn.example.com", "ms": 412, "it": "script" } ] }
One entry per slow host: hostname, duration in milliseconds, and the initiator type (it) — script, img, css, and so on. If the page exposes an experiment, the event also carries experiment and variant, which turns out to be handy for checking whether a variant ships a heavier tag than its control.
On the read side, an aggregator pulls the most recent of these events for the period — capped at 1,200 events so the report stays bounded on any site — and builds three views:
By host. Hits, total milliseconds, and average milliseconds per host, top 40 by frequency. This is the table you sort by average to find the one CDN that's having a bad month.
By vendor. Hosts are tagged with a heuristic vendor label — googletagmanager.com and gstatic.com both roll up to Google, fbcdn.net to Meta, and similarly for Cloudflare, Amazon, Microsoft, Stripe, Intercom, and Segment, with everything else under Other. Companies spread their delivery across many hostnames; the vendor rollup answers "how much of my third-party time is one company?" without you maintaining a mapping.
By experiment. When events carry experiment context, a per-variant event count, so you can spot a variant that's disproportionately represented in slow-resource reports.
The whole thing is a read-time aggregation over recent events rather than a persistent rollup — third-party questions are bursty ("we added a tag last week, what did it cost?") and the bounded sample keeps it fast enough that precomputing hasn't been worth it yet.
The quality signals around it
Performance rarely degrades alone, so the same dashboard carries the adjacent quality reports, each behind its own toggle:
- JavaScript errors, grouped by message, with stack fingerprints (
stack_fp, derived at ingest from the stack when one is sent) so the same error from minified bundles groups together instead of splintering by line number. - CSP reports, broken down by directive and blocked URI — which pairs nicely with third-party timing, since the same audit that finds slow tags tends to find unauthorized ones.
- Fetch latency (
$fetch_latency) for API calls the page makes, and long tasks for main-thread stalls.
All of it lands in the same events pipeline as everything else: queue-backed ingest, so an error storm on your site doesn't take the dashboard down with it.
Workflows this is built for
Two loops I run on Tracely's own site, which shaped the design:
The regression hunt. After a deploy, open Performance with the same range length as the pre-deploy period, sort worst LCP paths, and check whether the slow paths are the ones you touched. The path rollups make this a couple of seconds regardless of traffic.
The third-party audit. Quarterly-ish, open the vendor rollup and ask whether each vendor's milliseconds still buy anything. It is remarkable how often the answer is a tag someone added for a campaign that ended.
If you're coming from a lab-tools-only workflow, the mental shift is that field data is noisier but honest — and once it's rolled up by path and hour, it's fast enough to check as casually as you check pageviews. There's more on how the dashboards fit together on the features page, and if you want these posts as they land, the feed is here.
Try Tracely on your site
One snippet, cookieless, first-party. See your traffic in minutes without handing it to an ad network.
Start freeGet new posts by email
Short product notes twice a week plus the occasional deep dive. No tracking pixels in the emails, and you can unsubscribe anytime.
Your address is used only to send new posts. No open tracking.