By Performate
k6 Browser Module: When to Add Real Browser Metrics to API Load Tests
k6 browser module for Core Web Vitals + API latency: Chromium-based scenarios, when to pair with HTTP tests, and official k6 browser docs.
Your API load test shows p95 under 400ms—yet users complain the checkout page feels slow. The gap is often client-side work: JavaScript bundles, layout, and third-party tags that HTTP-only k6 never executes. The k6 browser module drives real Chromium so you can measure Core Web Vitals alongside backend latency—without replacing your HTTP scenarios.
Browser-augmented load testing is not "run everything in Selenium." It is a targeted layer for flows where rendering dominates perceived performance, paired with HTTP tests that scale cheaply. In this guide you will learn when browser metrics matter, how to combine browser and API scenarios in one k6 run, and which signals justify the extra CPU cost—not just a Lighthouse run on localhost.
Why HTTP-only tests miss user-visible latency
Backend SLOs can be green while the experience degrades:
- Large JS bundles block interactivity even when
/api/cartis fast. - Client-side routing triggers waterfall fetches HTTP scripts model as isolated calls—not as a real navigation graph.
- Third-party scripts (analytics, payments iframe) add main-thread work invisible to plain
http.get. - Hydration and SSR shift work between server and browser; API-only tests see only one side.
- Core Web Vitals gates (LCP, INP) require browser APIs—
http_req_durationcannot substitute.
Think of performance like a restaurant: API tests measure kitchen ticket time; browser tests measure plate-to-table—including garnish and presentation.
When to pair browser with HTTP scenarios
Use browser k6 for critical UX paths (login, checkout shell, dashboard first paint) and HTTP k6 for high-volume API surfaces (performance budget frontend + API). Align thresholds with p95 vs p99 on both layers.
Practical k6 implementation: hybrid browser + HTTP scenarios
Run a low-VU browser scenario for Core Web Vitals and a high-rate HTTP scenario for API throughput in the same options.scenarios block.
Example script (illustrative—not a production-ready test). Requires k6 with browser support enabled. Adapt URLs and selectors to your app.
What this example demonstrates:
- Parallel browser and HTTP scenarios:
browserexecutor at 2 VUs for checkout UI;constant-arrival-rateat 30 req/s for cart API. - Web Vitals-style checks:
page.gotowith wait and locator checks approximate LCP-critical path timing. - Shared base URL via env: one staging target for both layers—easier correlation in reports.
- Separate thresholds per layer: stricter HTTP SLOs; relaxed browser
p95reflecting Chromium overhead.
import http from 'k6/http';
import { browser } from 'k6/browser';
import { check, sleep } from 'k6';
const BASE = __ENV.APP_BASE || 'https://staging.example.com';
export const options = {
scenarios: {
checkout_browser: {
executor: 'constant-vus',
vus: 2,
duration: '5m',
options: { browser: { type: 'chromium' } },
exec: 'browserCheckout',
tags: { layer: 'browser', flow: 'checkout' },
},
cart_api: {
executor: 'constant-arrival-rate',
rate: 30,
timeUnit: '1s',
duration: '5m',
preAllocatedVUs: 10,
maxVUs: 40,
exec: 'cartApi',
tags: { layer: 'http', route: 'cart' },
},
},
thresholds: {
'http_req_duration{layer:http}': ['p(95)<450'],
'browser_web_vital_lcp{layer:browser}': ['p(95)<2500'],
},
};
export async function browserCheckout() {
const page = await browser.newPage();
try {
await page.goto(`${BASE}/checkout`, { waitUntil: 'networkidle' });
await page.locator('[data-testid="pay-button"]').waitFor({ state: 'visible' });
check(page, {
'checkout shell visible': () => true,
});
sleep(2);
} finally {
await page.close();
}
}
export function cartApi() {
const res = http.get(`${BASE}/api/v1/cart`, {
headers: { Authorization: `Bearer ${__ENV.TOKEN}` },
tags: { layer: 'http', route: 'cart' },
});
check(res, { 'cart 2xx': (r) => r.status >= 200 && r.status < 300 });
}
Patterns that work
- Keep browser VUs low (1–5)—Chromium is CPU-heavy; scale APIs with HTTP (k6 scenario types).
- Stable selectors: prefer
data-testidover brittle CSS—same discipline as E2E tests. - Run browser smokes in CI on schedule, not every PR—cost and flake management.
- Correlate tags
layer:browser|httpin Grafana or Performate reports (k6 observability tags).
Anti-patterns to avoid
- Replacing all HTTP tests with browser—cost explodes, signal drowns.
- Maxing browser VUs to "simulate load"—use HTTP arrival rate for that question.
- Ignoring auth in browser flows—mirror cookie or token setup from real users.
Pro tip (example command):
K6_BROWSER_ENABLED=true k6 run hybrid-checkout.js --summary-trend-stats="p(95),p(99)"
What this command demonstrates: enables Chromium-backed scenarios locally or in CI agents that support browser binaries.
Decision framework: HTTP only vs hybrid vs browser-only
| Situation | Recommended action |
|---|---|
| Internal JSON API, no UI gate | HTTP k6 only |
| SPA checkout with vitals SLO | Hybrid: low-VU browser + HTTP API load |
| Marketing landing LCP regression | Browser-only smoke; 1–3 VUs |
| Mobile WebView shell + APIs | Hybrid; tag client:webview on HTTP if applicable |
| CI on every commit | HTTP smoke; weekly browser job |
Use HTTP only if releases gate on API latency without client vitals commitments.
Use hybrid if product tracks LCP/INP and API SLOs together—most ecommerce and SaaS dashboards.
Use browser-only if the risk is entirely front-end (static site, landing) with minimal API surface.
Observability, documentation, and next steps
Hybrid tests only help if teams know which layer failed. Before scaling:
- Document browser VU cap, agent CPU requirements, and Chromium install path in runbook.
- Set alerts on
browser_web_vital_*thresholds separate from HTTP—different owners may apply. - Correlate
layer:*tags with RUM (Real User Monitoring) for the same routes. - Automate HTTP smoke every merge; schedule browser smokes (CI/CD load testing).
- Archive scenario JSON and selector list when UI refactors—update browser script in same PR.
How Performate simplifies hybrid browser + API testing
Jumping between Lighthouse, HTTP k6, and spreadsheets splits ownership. Below is a concrete workflow example for checkout hybrid testing.
Example: one workspace for cart API load and checkout vitals
- Import Postman cart API requests and keep browser checkout as a documented URL list with selectors in scenario notes. Problem solved: API truth stays in collection; browser path is explicit.
- Create HTTP scenario
cart_apiat 30 req/s with bearer token from env. Problem solved: scalable load without Chromium cost. - Add browser scenario
checkout_shellat 2 VUs with target URL and threshold notes for LCP. Problem solved: vitals path visible to QA without separate tool. - Tag scenarios
layer:httpandlayer:browserfor filtered reports. Problem solved: same tag model as the k6 example above. - Run hybrid suite before release and review integrated report with product/design. Problem solved: one export for backend and frontend perf sign-off.
- Export k6 script for CI HTTP smoke; keep browser job on nightly agents with
K6_BROWSER_ENABLED=true. Problem solved: fast PR gates plus vitals coverage.
That workflow maps to the cta: imports through results in one desktop flow.
Closing takeaway
Browser k6 is a precision instrument, not a replacement for HTTP load tests. Pair low-VU Chromium scenarios on critical UX paths with high-rate API scenarios, tag layers separately, and gate releases on both when Core Web Vitals are part of the contract.
Run this week's hybrid smoke on checkout—note whether API p95 or LCP moved first when users reported slowness.
Ready to optimize your API performance?
Explore how Performate simplifies k6 load testing—from imports to results—so your team ships performance confidence faster.