Skip to content

Performance Testing with k6 and Lighthouse CI

DodaTech Updated 2026-06-23 8 min read

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:

  1. Check the artifact for the failing metric
  2. Compare against previous runs to identify the change
  3. Use git bisect to find the commit that introduced the regression
  4. 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

  1. Running load tests against production: Load tests generate traffic that can degrade performance for real users. Always test against a staging environment.

  2. Not using realistic test data: Synthetic data with unrealistic sizes or distributions produces misleading results. Use data that mirrors production patterns.

  3. 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.

  4. 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.

  5. Setting thresholds too loosely: Thresholds that always pass provide no value. Start with tight thresholds and relax them only with documented justification.

  6. 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

  1. What is the difference between k6 and Lighthouse CI?
  2. How do you set a pass/fail threshold in k6?
  3. What is the purpose of staging phases in a k6 load test?
  4. How does Lighthouse CI relate to the Lighthouse browser extension?
  5. 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

Can k6 test authenticated endpoints?

Yes, k6 can set cookies, headers, and tokens. Use the http.cookieJar() API or set Authorization headers in the params object for authenticated requests.

How many virtual users should I test with?

Start with 50 concurrent users for a typical web application and scale up based on your production traffic patterns. Monitor response times at each level to identify the breaking point.

Does Lighthouse CI require a build step?

Lighthouse CI can test any deployed URL. For static sites, build the site first so Lighthouse CI can test the built output. For dynamic sites, test the staging deployment.

Can I run k6 in distributed mode?

Yes, k6 supports distributed execution using k6-operator on Kubernetes. For most use cases, a single node running 200-500 virtual users is sufficient.

What does DodaTech use for Performance Testing?

DodaTech uses k6 for API load testing with a staging environment that mirrors production. Lighthouse CI runs on every pull request with budgets enforced. Both are integrated into GitHub Actions.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro