Frontend web vitals under load
A backend can look healthy (p95 API latency within SLO, error rate near zero) while users get slow page loads, high LCP and layout shifts. Core Web Vitals depend on what happens in the browser: JS execution, render, third-party scripts, and how slow API responses add up in the load time users see. This page shows how to measure front-end timing while the backend is under MaxoPerf protocol load.
Before you start
Section titled “Before you start”- Read Core Web Vitals definitions to learn LCP, CLS, INP, and TTFB.
- Read Browser vs protocol load to learn the two layers this page combines.
- Read Playwright performance testing for the web-vitals capture script pattern.
Why front-end metrics degrade under backend load
Section titled “Why front-end metrics degrade under backend load”When the backend is under load:
- TTFB increases. The server takes longer to respond, so the browser waits longer before it can start rendering.
- LCP is delayed. If the largest contentful element depends on an API response (JSON data, a dynamic image URL, or a server-side rendered block), a slow API pushes LCP later.
- INP can worsen. If JavaScript makes API calls on interaction, a slow backend delays the response to each interaction and raises the INP score.
- CLS may spike. If content placeholders collapse and re-expand when API data finally arrives, layout shifts add up.
A protocol-only load test shows none of these effects. You see them only when you run a browser alongside the protocol load.
The combined measurement pattern
Section titled “The combined measurement pattern”The pattern has three steps:
- Apply backend load. Run a MaxoPerf protocol load test at realistic concurrency (the load of your expected peak traffic).
- Sample the front end. Run a few browser checks during the load to capture web vitals.
- Compare idle vs loaded. Compare web vitals under load with your baseline (idle or low-load) measurements to see how much peak load degrades them.
Option A: Taurus selenium executor (in-platform)
Section titled “Option A: Taurus selenium executor (in-platform)”To run the browser check and the protocol load in a single MaxoPerf run, use the hybrid load architecture. The selenium executor runs browser VUs alongside protocol VUs in the same Taurus YAML. In your Selenium Python script, capture TTFB and LCP from window.performance:
from selenium import webdriverfrom selenium.webdriver.common.by import Byfrom selenium.webdriver.support.ui import WebDriverWaitfrom selenium.webdriver.support import expected_conditions as EC
def test_dashboard_web_vitals(driver): """Capture TTFB, load time, and (optionally) LCP while backend is under load.""" driver.get("https://app.example.com/dashboard")
# Wait for the main content to render WebDriverWait(driver, 20).until( EC.presence_of_element_located((By.CSS_SELECTOR, "[data-testid='run-list']")) )
# Capture Navigation Timing (TTFB + load) timing = driver.execute_script(""" const e = performance.getEntriesByType('navigation')[0]; return { ttfb: e.responseStart - e.requestStart, domContentLoaded: e.domContentLoadedEventEnd - e.startTime, loadComplete: e.loadEventEnd - e.startTime }; """)
# Log as custom metrics visible in MaxoPerf results (Taurus picks up stdout) print(f"TTFB: {timing['ttfb']:.0f} ms") print(f"DOMContentLoaded: {timing['domContentLoaded']:.0f} ms") print(f"LoadComplete: {timing['loadComplete']:.0f} ms")
# Assert on your SLO assert timing['ttfb'] < 200, f"TTFB {timing['ttfb']:.0f} ms exceeded 200 ms SLO" assert timing['loadComplete'] < 3000, f"Load {timing['loadComplete']:.0f} ms exceeded 3 s SLO"Taurus picks up the print() calls and MaxoPerf shows them in the run output. Assertion failures count as errors in the MaxoPerf result.
Option B: Playwright alongside MaxoPerf protocol load (external)
Section titled “Option B: Playwright alongside MaxoPerf protocol load (external)”To keep the browser measurement outside MaxoPerf, run it from CI or locally while the MaxoPerf protocol test is running:
import { chromium } from 'playwright';
interface WebVitals { ttfb: number; lcp: number | null; cls: number | null; inp: number | null;}
async function measureUnderLoad(targetUrl: string): Promise<WebVitals> { const browser = await chromium.launch({ headless: true }); const context = await browser.newContext(); const page = await context.newPage();
const vitals: Partial<WebVitals> = {};
// Inject web-vitals library before navigation await page.addInitScript(() => { (window as any).__vitals = {}; });
// Navigate and wait for load const start = performance.now(); const response = await page.goto(targetUrl, { waitUntil: 'networkidle' }); const ttfb = (response?.timing().responseStart ?? 0);
vitals.ttfb = ttfb;
// Collect LCP via PerformanceObserver (requires page interaction to trigger) const lcp = await page.evaluate(() => new Promise<number | null>((resolve) => { let lcpValue: number | null = null; const observer = new PerformanceObserver((list) => { const entries = list.getEntries(); lcpValue = entries[entries.length - 1].startTime; }); observer.observe({ type: 'largest-contentful-paint', buffered: true }); // Give time for LCP to finalize setTimeout(() => resolve(lcpValue), 3000); }) );
vitals.lcp = lcp;
console.log(`TTFB: ${vitals.ttfb?.toFixed(0)} ms`); console.log(`LCP: ${vitals.lcp?.toFixed(0)} ms`);
await browser.close(); return vitals as WebVitals;}
// Run during the load test steady statemeasureUnderLoad('https://app.example.com/dashboard') .then(v => { if (v.lcp && v.lcp > 2500) { console.error(`LCP ${v.lcp.toFixed(0)} ms EXCEEDS 2.5 s threshold (poor)`); process.exit(1); } console.log('Web vitals within thresholds.'); });Core Web Vitals thresholds
Section titled “Core Web Vitals thresholds”Use these Google-defined thresholds to grade results:
| Metric | Good | Needs improvement | Poor |
|---|---|---|---|
| LCP (Largest Contentful Paint) | ≤ 2.5 s | 2.5–4 s | > 4 s |
| INP (Interaction to Next Paint) | ≤ 200 ms | 200–500 ms | > 500 ms |
| CLS (Cumulative Layout Shift) | ≤ 0.1 | 0.1–0.25 | > 0.25 |
| TTFB (Time to First Byte) | ≤ 800 ms | 800 ms–1.8 s | > 1.8 s |
| FCP (First Contentful Paint) | ≤ 1.8 s | 1.8–3 s | > 3 s |
For full metric definitions, see the Core Web Vitals glossary.
Reading web vitals results in MaxoPerf
Section titled “Reading web vitals results in MaxoPerf”When you run browser VUs (via the selenium executor) alongside protocol VUs in a single MaxoPerf run:
- Interaction timings appear in the Overview tab as labeled response times. Each
driver.get()andWebDriverWaitstep creates a labeled timing entry. - Assertion failures from
assert timing['loadComplete'] < 3000count toward the error rate. That is what you want, because a failed web-vitals assertion is a real failure. - Throughput for browser VUs is measured in iterations per second (completed browser flows per second), separate from the protocol VU RPS.
- Compare the protocol VU latency chart with the browser timing chart. If the two rise together, the backend slowdown is causing the front-end degradation.
Do / don’t
Section titled “Do / don’t”Do:
- Measure web vitals under load, not only at idle. Idle measurements mislead you about production.
- Assert on TTFB and LCP in your browser script so that a threshold violation shows up as an error in the MaxoPerf result.
- Run the browser check during the steady-state phase of the load run (after ramp-up), not during ramp-up when load is still building.
Don’t:
- Assume good protocol-level p95 latency means good LCP. They measure different things.
- Skip CLS measurement. Protocol tests cannot see layout shifts, and shifts can ruin the user experience when API data arrives slowly.
- Treat a single browser sample as the truth. Run 3–5 browser iterations and use the median to reduce noise.
Where to go next
Section titled “Where to go next”- Hybrid load architecture: the full pattern for combining browser and protocol VUs in one MaxoPerf run.
- Playwright performance testing: web-vitals capture scripts and how Playwright pairs with MaxoPerf.
- Selenium performance testing: running browser VUs inside MaxoPerf.
- Core Web Vitals glossary: metric definitions, browser support, and measurement caveats.
- Foundations: Core metrics explained: latency, throughput, and error rate in load testing.