Performance Testing with K6 and Playwright
A step-by-step guide from setup to execution—combining functional browser journeys with realistic performance testing.
Why Combine K6 with Playwright?
Modern web applications must be fast and functionally correct under real-world traffic. Playwright is excellent for end-to-end verification, while k6 is purpose-built for load and performance testing. K6’s experimental browser module provides a Playwright-inspired API for browser-level performance checks.
Pairing the tools lets teams reuse selectors and user flows, validate critical journeys such as login or checkout, capture browser metrics alongside load metrics, and run tests from CI/CD.
Prerequisites and Setup
You’ll need Node.js v16 or later with npm, a code editor, optional Git, basic JavaScript and selector knowledge, and administrator or sudo access for k6 and browser dependencies.
Initialize a Node.js project
mkdir k6-playwright-perf-test
cd k6-playwright-perf-test
npm init -yInstall Playwright
npm install -D @playwright/test
npx playwright install --with-depsKeep a Playwright functional suite in the repository so the journey can be verified before it is performance-tested.
Install k6
# macOS
brew install k6
# Windows
choco install k6
# Debian / Ubuntu
sudo apt-get update
sudo apt-get install k6
# Docker alternative
docker pull grafana/k6
k6 versionEnable the browser module
Modern k6 releases bundle the experimental browser module. A recent release such as k6 v0.47.0 or later is recommended. Older releases may require an xk6 custom build:
go install go.k6.io/xk6/cmd/xk6@latest
xk6 build --with github.com/grafana/xk6-browser@latestRecommended project structure
k6-playwright-perf-test/
├── playwright-tests/
│ └── login.spec.js
├── k6-tests/
│ └── login.perf.js
├── package.json
└── playwright.config.jsWriting the Playwright Functional Test
First validate the login flow functionally, then translate the same selectors and steps into the k6 script.
// playwright-tests/login.spec.js
const { test, expect } = require('@playwright/test');
test('user can log in', async ({ page }) => {
await page.goto('https://test.k6.io/my_messages.php');
await page.fill('#login', 'admin');
await page.fill('#password', '123');
await page.click('input[type="submit"]');
await expect(page.locator('h3')).toContainText('Welcome, admin!');
});npx playwright test playwright-tests/login.spec.jsWriting the K6 Performance Test
import { browser } from 'k6/experimental/browser';
import { check } from 'k6';
export const options = {
scenarios: {
ui: {
executor: 'shared-iterations',
vus: 5,
iterations: 10,
options: { browser: { type: 'chromium' } },
},
},
thresholds: {
checks: ['rate==1.0'],
browser_http_req_duration: ['p(95)<2000'],
},
};
export default async function () {
const page = browser.newPage();
try {
await page.goto('https://test.k6.io/my_messages.php');
await page.locator('#login').type('admin');
await page.locator('#password').type('123');
await Promise.all([
page.waitForNavigation(),
page.locator('input[type="submit"]').click(),
]);
const heading = page.locator('h3');
check(heading, {
'welcome message is visible': (h) =>
h.textContent().includes('Welcome, admin!'),
});
} finally {
page.close();
}
}Key concepts: browser.newPage() creates a browser page, page.locator() mirrors Playwright’s selector model, scenarios define the load shape, thresholds define objective pass/fail criteria, and check() records assertions without acting like a test-runner exception.
Configuring Test Options
export const options = {
scenarios: {
ui: {
executor: 'ramping-vus',
startVUs: 1,
stages: [
{ duration: '30s', target: 5 },
{ duration: '1m', target: 5 },
{ duration: '30s', target: 0 },
],
options: { browser: { type: 'chromium' } },
},
},
thresholds: {
browser_http_req_duration: ['p(90)<1500', 'p(95)<2500'],
checks: ['rate>0.95'],
},
};- Executors: Choose shared-iterations, per-vu-iterations, constant-vus, ramping-vus, constant-arrival-rate, or ramping-arrival-rate according to the desired concurrency or arrival rate.
- Stages: Ramp VUs gradually. Browser VUs are resource-intensive; keep counts modest, typically under 10–20 concurrent browser VUs per machine.
- Thresholds: Tie automated pass/fail behavior to measurable SLAs.
Executing the Performance Test
k6 run k6-tests/login.perf.js
# Docker
docker run --rm -i -v $(pwd):/scripts grafana/k6 \
run /scripts/k6-tests/login.perf.js
# JSON output
k6 run --out json=results.json k6-tests/login.perf.js
# InfluxDB output
k6 run --out influxdb=http://localhost:8086/k6 k6-tests/login.perf.jsAnalyzing and Interpreting Results
checks.........................: 100.00% ✓ 10 ✗ 0
browser_http_req_duration......: avg=812ms p(95)=1.3s
browser_web_vital_lcp...........: avg=1.1s
iteration_duration..............: avg=2.3s
vus..............................: 5- checks: The pass rate of all
check()assertions. - browser_http_req_duration: Time taken by browser-initiated HTTP requests.
- browser_web_vital_*: Browser-level LCP, FCP, and CLS metrics when supported.
- iteration_duration: Total duration of a complete script iteration.
- Threshold results: k6 returns a non-zero exit status when a threshold fails, making it suitable for CI/CD gates.
Best Practices and Common Pitfalls
- Keep browser-level VUs low. Use protocol-level k6 tests for high concurrency and browser tests for representative journeys.
- Separate concerns. Use Playwright for strict functional assertions and cross-browser coverage; use k6 for performance-focused checks.
- Use realistic think-time. Prefer wait-for-selector patterns or carefully chosen pauses to mimic user pacing.
- Set meaningful thresholds tied to actual service-level objectives.
- Run in CI/CD and use k6’s exit code as a quality gate.
- Isolate test data with unique users or records per VU and iteration.
- Monitor the test runner. CPU and memory limits can bottleneck the generator before the target system.
Conclusion
Combining k6 with a Playwright-style browser API provides a consistent way to validate functional correctness and real-world performance. Verify the flow with Playwright, translate it to a k6 browser script, define a realistic scenario and thresholds, execute it, and use the resulting metrics to catch regressions early.
Frequently Asked Questions
Is k6’s browser module the same as Playwright?
No. It is a Playwright-inspired API designed for performance testing in k6. Playwright remains the dedicated functional and E2E tool.
Can I run thousands of virtual users with the browser module?
Browser-level VUs are resource-heavy. Use protocol-level k6 tests for high-concurrency load and browser-level tests for smaller, representative populations.
Do I need Playwright installed to use k6’s browser module?
No. k6 runs its browser module independently. Playwright is complementary and useful for validating flows and reusing selectors.
Can these tests run in a CI/CD pipeline?
Yes. Both tools are CLI-driven and return process exit codes suitable for GitHub Actions, GitLab CI, Jenkins, and similar systems.