Performance Testing with k6 and Lighthouse CI
In this tutorial, you will learn how to set up automated Performance Testing using k6 for backend load testing and Lighthouse CI for front-end performance audits. Together these tools provide comprehensive performance coverage. DodaTech runs both in CI to catch regressions in API response times and page rendering speed before they reach production.
What You Will Learn
- How to write and run k6 load tests for API endpoints
- How to set up Lighthouse CI for automated front-end performance audits
- How to integrate both tools into a CI/CD pipeline
- How to interpret results and set pass/fail thresholds
Why It Matters
Manual Performance Testing is inconsistent and happens too late. Automated performance tests in CI catch regressions immediately when they are introduced, not after deployment. DodaTech found that automated Performance Testing reduced production performance incidents by 80 percent.
Real-World Use Case
The DodaZIP API team introduced a new database query that worked correctly but increased p95 response time from 200ms to 2 seconds. The k6 test in CI failed because the response time exceeded the 500ms threshold, blocking the pull request. The team optimized the query before merging, preventing a production outage.
Prerequisites
You should understand HTTP API concepts and have basic JavaScript programming skills. Familiarity with CI/CD pipelines and Lighthouse Audits is helpful.
Step-by-Step Tutorial
Step 1: Install k6
k6 is a modern load testing tool. Install it locally to write and test scripts.
# Install k6 on Ubuntu
sudo gpg -k
sudo gpg --no-default-keyring --keyring /usr/share/keyrings/k6-archive-keyring.gpg --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys C5AD17C747E3415A3642D57D77C6C491D6AC1D69
echo "deb [signed-by=/usr/share/keyrings/k6-archive-keyring.gpg] https://dl.k6.io/deb stable main" | sudo tee /etc/apt/sources.list.d/k6.list
sudo apt-get update
sudo apt-get install k6
# Verify installation
k6 version
Expected output: k6 v0.52.0 (or similar version number).
Step 2: Write a k6 Load Test
Create a k6 script that simulates real user traffic against your API.
// load-test.js
import http from 'k6/http';
import { check, sleep } from 'k6';
import { Rate, Trend } from 'k6/metrics';
// Custom metrics
const errorRate = new Rate('errors');
const responseTime = new Trend('response_time');
export const options = {
stages: [
{ duration: '2m', target: 50 }, // Ramp up to 50 users
{ duration: '5m', target: 50 }, // Stay at 50 users
{ duration: '2m', target: 100 }, // Ramp up to 100 users
{ duration: '5m', target: 100 }, // Stay at 100 users
{ duration: '2m', target: 0 }, // Ramp down
],
thresholds: {
http_req_duration: ['p(95)<500'], // 95% of requests under 500ms
errors: ['rate<0.1'], // Less than 10% error rate
},
};
const BASE_URL = 'https://api-staging.dodatech.com';
export default function () {
const params = {
headers: { 'Content-Type': 'application/json' },
tags: { endpoint: 'products' },
};
// Test GET /api/products
const getRes = http.get(`${BASE_URL}/api/products`, params);
check(getRes, {
'GET products status is 200': (r) => r.status === 200,
'GET products response time < 300ms': (r) => r.timings.duration < 300,
});
responseTime.add(getRes.timings.duration);
errorRate.add(getRes.status !== 200);
sleep(1);
// Test POST /api/orders
const payload = JSON.stringify({
productId: 42,
quantity: 1,
});
const postRes = http.post(`${BASE_URL}/api/orders`, payload, params);
check(postRes, {
'POST orders status is 201': (r) => r.status === 201,
'POST orders response time < 500ms': (r) => r.timings.duration < 500,
});
responseTime.add(postRes.timings.duration);
errorRate.add(postRes.status !== 201);
sleep(2);
}
Step 3: Run a k6 Test Locally
Execute the load test script to see results.
k6 run load-test.js
Expected output: A summary table showing metrics like http_req_duration with average, p50, p90, p95 values, and thresholds (pass/fail). If thresholds are exceeded, k6 exits with a non-zero code.
Step 4: Set Up Lighthouse CI
Lighthouse CI runs Lighthouse audits and compares results against a performance budget.
// lighthouserc.js
module.exports = {
ci: {
collect: {
numberOfRuns: 3,
url: [
'https://staging.dodatech.com',
'https://staging.dodatech.com/products',
'https://staging.dodatech.com/docs'
],
settings: {
preset: 'desktop',
throttlingMethod: 'simulate'
}
},
assert: {
budgetsFile: './budget.json',
assertions: {
'categories:performance': ['warn', {minScore: 0.9}],
'categories:accessibility': ['error', {minScore: 0.9}],
'resource-summary': ['error', {budgets: './budget.json'}]
}
},
upload: {
target: 'filesystem',
outputDir: './lhci-reports'
}
}
};
# Run Lighthouse CI
npx lhci autorun
Expected output: URLs are tested, results are compared against budgets, and the exit code indicates pass or fail.
Step 5: Integrate into CI/CD
Create a GitHub Actions workflow that runs both k6 and Lighthouse CI on every pull request.
# .github/workflows/performance-test.yml
name: Performance Tests
on: [pull_request]
jobs:
k6:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Run k6 load test
uses: grafana/k6-action@v0.3.0
with:
filename: tests/load-test.js
flags: --out json=results.json
- name: Upload k6 results
uses: actions/upload-artifact@v4
with:
name: k6-results
path: results.json
lighthouse:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20
- name: Build
run: npm run build
- name: Run Lighthouse CI
run: npx lhci autorun
env:
LHCI_GITHUB_APP_TOKEN: ${{ secrets.LHCI_GITHUB_APP_TOKEN }}
Expected behavior: The workflow runs two parallel jobs. k6 tests the API performance, Lighthouse CI tests the front-end performance. Both must pass for the PR to be approved.
Step 6: Create a Performance Dashboard
Aggregate k6 and Lighthouse results into a dashboard for trend analysis.
# Export Lighthouse results
npx lhci collect --url https://dodatech.com --numberOfRuns 3
npx lhci upload --target filesystem --outputDir ./reports
# Parse k6 results
k6 run load-test.js --out json=results.json
cat results.json | python3 -c "
import sys, json
for line in sys.stdin:
data = json.loads(line)
if data['type'] == 'Point' and 'http_req_duration' in data['metric']:
print(f\"{data['data']['time']}: {data['metric']} = {data['data']['value']}\")
"
Step 7: Set Up Scheduled Performance Tests
Run performance tests on a schedule to catch regressions introduced by external dependencies.
# Scheduled performance test
name: Nightly Performance
on:
schedule:
- cron: '0 6 * * *' # Run at 6 AM daily
jobs:
performance:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Run tests
run: |
k6 run tests/load-test.js
npx lhci autorun
- name: Notify on regression
if: failure()
uses: slackapi/slack-github-action@v1
with:
payload: '{"text": "Performance regression detected in nightly tests"}'
Step 8: Analyze and Act on Results
Create a process for when performance tests fail:
- Check the artifact for the failing metric
- Compare against previous runs to identify the change
- Use git bisect to find the commit that introduced the regression
- Create a fix or rollback the change
Learning Path
flowchart LR A[Performance Budgets] --> B[Performance Testing] B --> C[CI/CD Integration] B --> D[Real User Monitoring] C --> D style B fill:#4f46e5,color:#fff style A fill:#6366f1,color:#fff style C fill:#6366f1,color:#fff
Common Errors
Running load tests against production: Load tests generate traffic that can degrade performance for real users. Always test against a staging environment.
Not using realistic test data: Synthetic data with unrealistic sizes or distributions produces misleading results. Use data that mirrors production patterns.
Testing only happy paths: Your performance test should include error scenarios, edge cases, and authentication failures to test how the system handles non-happy-path requests.
Ignoring warm-up time: The first few requests to a freshly deployed service are slow due to JIT Compilation and cache population. Include a warm-up phase in your test.
Setting thresholds too loosely: Thresholds that always pass provide no value. Start with tight thresholds and relax them only with documented justification.
Not tracking trends over time: A single test run tells you if a threshold passes today. Trend analysis shows whether performance is improving or degrading gradually.
Practice Questions
- What is the difference between k6 and Lighthouse CI?
- How do you set a pass/fail threshold in k6?
- What is the purpose of staging phases in a k6 load test?
- How does Lighthouse CI relate to the Lighthouse browser extension?
- Why should load tests run against staging instead of production?
Answers: 1. k6 tests API/backend performance under load; Lighthouse CI tests front-end rendering performance in a controlled environment. 2. Define thresholds in the options object, e.g., http_req_duration: ['p(95)<500']. 3. Staging phases gradually increase and decrease the number of virtual users, simulating realistic traffic patterns. 4. Both use the same Lighthouse engine, but Lighthouse CI runs programmatically and supports budget assertions for CI integration. 5. Load tests generate significant traffic that can degrade performance for real users and potentially trigger alerts.
Challenge
Set up a complete Performance Testing pipeline for a sample e-commerce application. Write a k6 script that tests the product listing API (GET) and checkout API (POST) with thresholds for p95 response time under 500ms and error rate under 5 percent. Add a Lighthouse CI configuration that tests the homepage, product page, and cart page with a budget of LCP under 2.5s and TBT under 200ms. Configure a GitHub Actions workflow that runs both tests on every pull request and on a nightly schedule.
FAQ
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro