Skip to main content

Overview

The Tinybird tracker integrates with Google’s web-vitals library to automatically capture Core Web Vitals metrics. Monitor real-world performance data from your users’ browsers.

Enabling Web Vitals

Add the web-vitals attribute to your tracker script:
<script
  src="https://unpkg.com/@tinybirdco/flock.js"
  data-token="YOUR_TRACKER_TOKEN"
  web-vitals="true"
></script>
You can also use data-web-vitals="true" - both formats are supported.

Tracked Metrics

The tracker automatically monitors all Core Web Vitals:

LCP

Largest Contentful PaintLoad performance - measures when the largest content element becomes visibleGood: < 2.5s

FCP

First Contentful PaintFirst paint time - when the first content appears on screenGood: < 1.8s

INP

Interaction to Next PaintInteraction responsiveness - delay between user action and visual responseGood: < 200ms

CLS

Cumulative Layout ShiftVisual stability - measures unexpected layout shiftsGood: < 0.1

TTFB

Time to First ByteServer responsiveness - time until browser receives first byteGood: < 500ms

How It Works

Implementation

From the source code (middleware/src/index.js:43-47):
var webVitals = null;
if (document.currentScript && 
    (document.currentScript.getAttribute('web-vitals') === 'true' || 
     document.currentScript.getAttribute('data-web-vitals') === 'true')) {
  webVitals = require('web-vitals')
}

Metric Collection

The tracker registers callbacks for each metric (middleware/src/index.js:331-359):
if (webVitals) {
  function sendMetric(metric) {
    try {
      const { country, locale } = getCountryAndLocale();
      _sendEvent('web_vital', {
        name: metric.name,
        value: metric.value,
        delta: metric.delta,
        rating: metric.rating,
        id: metric.id,
        navigationType: metric.navigationType,
        pathname: window.location.pathname,
        href: window.location.href,
        'user-agent': window.navigator.userAgent,
        locale,
        location: country,
        referrer: document.referrer,
      });
    } catch (error) {
      console.error('Error sending web vital:', error);
    }
  }

  if(webVitals.onCLS) webVitals.onCLS(sendMetric);
  if(webVitals.onFCP) webVitals.onFCP(sendMetric);
  if(webVitals.onLCP) webVitals.onLCP(sendMetric);
  if(webVitals.onTTFB) webVitals.onTTFB(sendMetric);
  if(webVitals.onINP) webVitals.onINP(sendMetric);
}

Event Data Structure

Each web vital is sent as a separate event with action: "web_vital":
{
  "timestamp": "2024-03-11T14:23:45.123Z",
  "action": "web_vital",
  "version": "1",
  "session_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "payload": {
    "name": "LCP",
    "value": 1250.5,
    "delta": 1250.5,
    "rating": "good",
    "id": "v3-1752067824029-4726354841567",
    "navigationType": "navigate",
    "pathname": "/products",
    "href": "https://example.com/products",
    "user-agent": "Mozilla/5.0...",
    "locale": "en-US",
    "location": "US",
    "referrer": "https://google.com"
  }
}

Payload Fields

name
string
Metric identifier: LCP, FCP, INP, CLS, or TTFB
value
number
The metric value in milliseconds (or unitless for CLS)
delta
number
Change since last report (for metrics that can update multiple times)
rating
string
Performance rating: good, needs-improvement, or poorBased on thresholds defined by Google:
  • LCP: Good < 2.5s, Poor > 4s
  • FCP: Good < 1.8s, Poor > 3s
  • INP: Good < 200ms, Poor > 500ms
  • CLS: Good < 0.1, Poor > 0.25
  • TTFB: Good < 500ms, Poor > 1500ms
id
string
Unique identifier for this metric instance
navigationType
string
Type of navigation: navigate, reload, back-forward, or prerender
pathname
string
Page path where the metric was recorded
href
string
Full URL where the metric was recorded

Example Metrics

Real-world examples from the README:
{"name":"LCP","value":68,"delta":68,"id":"v3-1752067824029-4726354841567","pathname":"/","href":"http://localhost:8081/"}
{"name":"TTFB","value":41.10000002384186,"delta":41.10000002384186,"id":"v3-1752067821037-7353415626830","pathname":"/","href":"http://localhost:8081/"}
{"name":"FCP","value":120,"delta":120,"id":"v3-1752067821037-7485331818919","pathname":"/","href":"http://localhost:8081/"}
{"name":"INP","value":0,"delta":0,"id":"v3-1752067821037-7066346355405","pathname":"/","href":"http://localhost:8081/"}
{"name":"CLS","value":0,"delta":0,"id":"v3-1752067821037-6535975895510","pathname":"/","href":"http://localhost:8081/"}

Interpreting the Metrics

Largest Contentful Paint measures when the main content loads.
ValueRatingStatus
< 2500msGood✅ Excellent
2500-4000msNeeds Improvement⚠️ Fair
> 4000msPoor❌ Needs work
Common issues:
  • Slow server response times
  • Large images without optimization
  • Render-blocking JavaScript/CSS
Example from data:
{"name":"LCP","value":68,"rating":"good"}
68ms is excellent!

When Metrics Are Reported

Reported as soon as the first byte is received from the server.
Reported when the first content pixel is painted.
Reported when the largest content element is painted. Can update multiple times if larger elements load later.
Reported after user interactions (clicks, taps, keyboard). Updates as more interactions occur.
Reported and updated throughout the page session as layout shifts occur.
Some metrics report multiple times as they get more accurate. The delta field shows the change since the last report.

Complete Example

<!DOCTYPE html>
<html>
  <head>
    <title>My Fast Website</title>
    
    <!-- Tinybird Analytics with Web Vitals -->
    <script
      defer
      src="https://unpkg.com/@tinybirdco/flock.js"
      data-token="p.eyJ1IjogImFiY2QxMjM0In0..."
      web-vitals="true"
    ></script>
  </head>
  <body>
    <h1>Welcome</h1>
    <p>Your content here...</p>
  </body>
</html>

Analyzing Web Vitals Data

Once you’re collecting Web Vitals, you can analyze them in your Tinybird workspace:

Query Examples

SELECT
  JSONExtractString(payload, 'name') as metric,
  avg(JSONExtractFloat(payload, 'value')) as avg_value,
  quantile(0.75)(JSONExtractFloat(payload, 'value')) as p75_value
FROM analytics_events
WHERE action = 'web_vital'
GROUP BY metric

Best Practices

Monitor Percentiles

Look at p75 and p95 percentiles, not just averages. These show what most users experience.

Segment by Device

Mobile devices often have different performance characteristics than desktop.

Track Over Time

Monitor trends to catch regressions early. Set up alerts for degradation.

Compare Pages

Identify which pages need optimization by comparing metrics across routes.

Troubleshooting

Check:
  1. web-vitals="true" attribute is present
  2. Script is loading successfully (check browser console)
  3. Events are reaching Tinybird (check data source)
  4. Browser supports Web Vitals API (modern browsers only)
Common causes:
  • Values are in milliseconds, not seconds (except CLS)
  • Some metrics update multiple times per page load
  • Different navigation types affect metrics
  • Browser extensions can impact measurements
Common causes:
  • Images without width/height attributes
  • Ads loading dynamically
  • Web fonts causing layout shifts (FOUT/FOIT)
  • Content injected above existing content

Browser Support

Web Vitals tracking requires modern browser APIs:
  • ✅ Chrome 90+
  • ✅ Edge 90+
  • ✅ Opera 76+
  • ⚠️ Safari (partial support)
  • ⚠️ Firefox (partial support)
The tracker gracefully degrades - if Web Vitals aren’t supported, page views and custom events still work.

Next Steps

Configuration

Explore advanced configuration options

Custom Events

Learn about tracking custom events

Build docs developers (and LLMs) love