Browser Testing
Measure real user experience under load with Chromium automation
Overview
Fusillade includes headless browser automation via Chromium for measuring real user experience while your backend is under load. Unlike HTTP tests which measure API response times, browser tests measure what users actually experience: page load times, DOM rendering, JavaScript execution, and visual completeness.
Hybrid Testing Pattern: Use HTTP workers to generate backend load while a small number of browser instances measure frontend performance.
Key Insight: API response times don't tell the whole story. A 50ms API response can still result in a 3-second page load due to rendering, JS execution, and resource loading.
Resource Note:
Each browser instance requires ~100-200MB of memory. Use 1-5 browser workers alongside many HTTP workers.
Performance Metrics
The key feature for load testing is page.metrics() which returns browser performance timing data.
page.metrics()Returns performance timing metrics object
Metrics Object Properties
navigationStart - When navigation began (timestamp)
domInteractive - DOM is ready for interaction
domComplete - DOM and all resources loaded
loadEventEnd - Load event handler completed
Key Performance Indicators
Time to Interactive (TTI) = domInteractive - navigationStart
DOM Complete = domComplete - navigationStart
Full Page Load = loadEventEnd - navigationStart
# Measuring Page Performance
export default function() {
const browser = chromium.launch();
const page = browser.newPage();
page.goto('https://example.com/dashboard');
// Get performance timing metrics
const perf = page.metrics();
// Calculate key metrics
const tti = perf.domInteractive - perf.navigationStart;
const domComplete = perf.domComplete - perf.navigationStart;
const fullLoad = perf.loadEventEnd - perf.navigationStart;
print(`Time to Interactive: ${tti}ms`);
print(`DOM Complete: ${domComplete}ms`);
print(`Full Page Load: ${fullLoad}ms`);
// Record as custom metrics for thresholds
metrics.histogramAdd('browser_tti', tti);
metrics.histogramAdd('browser_dom_complete', domComplete);
metrics.histogramAdd('browser_full_load', fullLoad);
browser.close();
}Hybrid Load Testing
The most powerful use of browser testing is combining it with HTTP load generation. HTTP workers stress your backend while browser workers measure the actual user experience.
# Hybrid Test: HTTP Load + Browser Monitoring
export const options = {
scenarios: {
// Generate API load with many HTTP workers
api_load: {
executor: 'constant-workers',
workers: 100,
duration: '5m',
exec: 'apiLoad',
},
// Measure user experience with few browser workers
browser_monitor: {
executor: 'constant-workers',
workers: 2,
duration: '5m',
exec: 'browserMonitor',
},
},
thresholds: {
// HTTP thresholds
'http_req_duration': ['p95 < 500'],
// Browser experience thresholds
'browser_tti': ['p95 < 2000'],
'browser_full_load': ['p95 < 5000'],
},
};
// HTTP load generation
export function apiLoad() {
http.get('https://api.example.com/data');
http.post('https://api.example.com/actions', JSON.stringify({ action: 'test' }));
sleep(0.1);
}
// Browser experience measurement
export function browserMonitor() {
const browser = chromium.launch();
const page = browser.newPage();
page.goto('https://example.com/dashboard');
const perf = page.metrics();
const tti = perf.domInteractive - perf.navigationStart;
const fullLoad = perf.loadEventEnd - perf.navigationStart;
metrics.histogramAdd('browser_tti', tti);
metrics.histogramAdd('browser_full_load', fullLoad);
check(page, {
'page loads under 5s': () => fullLoad < 5000,
'interactive under 2s': () => tti < 2000,
});
browser.close();
sleep(5); // Check every 5 seconds
}Browser API
chromium.launch()Launch a new headless Chromium browser instance
browser.newPage()Open a new browser page/tab
browser.close()Close the browser and release resources
Page API
page.goto(url)Navigate to URL and wait for load
page.content()Get page HTML content as a string
page.title()Get the page title (via document.title)
page.url()Get the current page URL
page.click(selector)Click an element matching the CSS selector
page.type(selector, text)Type text into an input element (appends to existing value)
page.fill(selector, text)Clear input value and type new text
page.evaluate(script)Execute JavaScript in page context and return the result
page.metrics()Get performance timing metrics (see Performance Metrics above)
page.screenshot()Capture a PNG screenshot, returns raw PNG bytes
page.waitForSelector(selector)Wait for an element matching the CSS selector to appear in the DOM
page.waitForNavigation()Wait for the current page navigation to complete
page.waitForTimeout(ms)Wait for specified number of milliseconds
page.setContent(html)Replace page content with given HTML string
page.focus(selector)Focus the element matching the CSS selector
page.select(selector, values)Select options in a <select> element by value array
page.getCookies()Get array of cookies [{name, value}, ...]
page.setCookie(cookie)Set a cookie {name, value, domain?, path?, secure?, maxAge?}
page.deleteCookie(name)Delete a cookie by name
page.queryAll(selector)Query all matching elements [{tag, text, id, className}, ...]
page.waitForResponse(urlPattern, [timeoutMs])Wait for network response matching URL pattern (default 30s)
page.close()Close the page/tab and release resources
# Page API Usage
export default function() {
const browser = chromium.launch();
const page = browser.newPage();
// Navigate to page
page.goto('https://example.com/login');
// Get page info
print(`Title: ${page.title()}`);
print(`URL: ${page.url()}`);
// Type into form fields (appends to existing value)
page.type('#username', 'testuser');
// Fill clears the input first, then types
page.fill('#password', 'secret123');
// Click submit button
page.click('button[type="submit"]');
// Wait for navigation to complete after form submission
page.waitForNavigation();
// Wait for a specific element to appear (useful for SPAs)
page.waitForSelector('.dashboard-content');
// Verify we landed on the right page
check(null, {
'redirected to dashboard': () => page.url().includes('/dashboard'),
'welcome message visible': () => page.content().includes('Welcome'),
});
// Execute JavaScript in the page context
const title = page.evaluate('document.title');
print(`Page title: ${title}`);
// Capture screenshot (returns PNG bytes)
const png = page.screenshot();
page.close();
browser.close();
}User Flow Testing
Test complete user journeys while measuring performance at each step.
export default function() {
const browser = chromium.launch();
const page = browser.newPage();
// Step 1: Load login page
page.goto('https://example.com/login');
let perf = page.metrics();
metrics.histogramAdd('login_page_load', perf.loadEventEnd - perf.navigationStart);
// Step 2: Fill and submit login form
page.fill('#username', 'testuser');
page.fill('#password', 'secret123');
const loginStart = Date.now();
page.click('button[type="submit"]');
page.waitForNavigation();
metrics.histogramAdd('login_submit_time', Date.now() - loginStart);
// Step 3: Verify dashboard loaded
perf = page.metrics();
metrics.histogramAdd('dashboard_load', perf.loadEventEnd - perf.navigationStart);
check(null, {
'logged in successfully': () => page.url().includes('/dashboard'),
'welcome message visible': () => page.content().includes('Welcome'),
});
// Step 4: Navigate to a data-heavy page
const dataStart = Date.now();
page.click('a[href="/reports"]');
page.waitForSelector('.report-table');
metrics.histogramAdd('reports_load', Date.now() - dataStart);
page.close();
browser.close();
}JavaScript Evaluation
Execute JavaScript in the page context to extract data or measure client-side performance.
export default function() {
const browser = chromium.launch();
const page = browser.newPage();
page.goto('https://example.com/app');
// Get browser-side performance data
const perfData = page.evaluate(`
const timing = performance.timing;
const paint = performance.getEntriesByType('paint');
return {
// Navigation timing
dns: timing.domainLookupEnd - timing.domainLookupStart,
tcp: timing.connectEnd - timing.connectStart,
ttfb: timing.responseStart - timing.requestStart,
download: timing.responseEnd - timing.responseStart,
domParsing: timing.domInteractive - timing.responseEnd,
// Paint timing
firstPaint: paint.find(p => p.name === 'first-paint')?.startTime || 0,
firstContentfulPaint: paint.find(p => p.name === 'first-contentful-paint')?.startTime || 0,
// Resource count
resourceCount: performance.getEntriesByType('resource').length,
};
`);
print(`First Contentful Paint: ${perfData.firstContentfulPaint}ms`);
print(`Resources loaded: ${perfData.resourceCount}`);
metrics.histogramAdd('browser_fcp', perfData.firstContentfulPaint);
metrics.histogramAdd('browser_ttfb', perfData.ttfb);
browser.close();
}Screenshots on Failure
Capture screenshots when checks fail to help debug issues. The page.screenshot() method returns raw PNG bytes.
export default function() {
const browser = chromium.launch();
const page = browser.newPage();
page.goto('https://example.com/checkout');
const perf = page.metrics();
const loadTime = perf.loadEventEnd - perf.navigationStart;
// If page is slow, capture screenshot for debugging
if (loadTime > 5000) {
const png = page.screenshot();
print(`WARNING: Slow page load (${loadTime}ms) - screenshot captured (${png.length} bytes)`);
}
// Check page content
const html = page.content();
const checksPassed = check(null, {
'checkout form visible': () => html.includes('checkout-form'),
'price displayed': () => html.includes('$'),
});
if (!checksPassed) {
const png = page.screenshot();
print(`Check failed - screenshot captured (${png.length} bytes)`);
}
browser.close();
}Best Practices
1. Use few browser workers (1-5) alongside many HTTP workers
2. Focus on page.metrics() for performance data
3. Record metrics with metrics.histogramAdd() for thresholds
4. Use page.waitForSelector() instead of fixed delays for dynamic content
5. Use page.waitForNavigation() after clicks that trigger page loads
6. Always close pages and browsers to prevent memory leaks
7. Capture screenshots on failures for debugging
8. Use page.fill() for form inputs (clears first) and page.type() to append
Browser Performance Thresholds
export const options = {
workers: 2,
duration: '5m',
thresholds: {
// Time to Interactive under 2 seconds
'browser_tti': ['p95 < 2000'],
// Full page load under 5 seconds
'browser_full_load': ['p95 < 5000'],
// First Contentful Paint under 1.5 seconds
'browser_fcp': ['p95 < 1500'],
// All checks must pass
'checks': ['rate > 0.99'],
},
};
export default function() {
const browser = chromium.launch();
const page = browser.newPage();
page.goto('https://example.com');
const perf = page.metrics();
metrics.histogramAdd('browser_tti', perf.domInteractive - perf.navigationStart);
metrics.histogramAdd('browser_full_load', perf.loadEventEnd - perf.navigationStart);
browser.close();
}