top of page

From E2E to Performance: Reusing Playwright Tests with Artillery and Supabase Tracing

  • techsandpartnershi
  • 20 hours ago
  • 5 min read

If you already have Playwright E2E tests, you're much closer to browser performance testing than you think. By combining Playwright, Artillery, Docker, and Supabase tracing, you can investigate real user journeys under load without writing synthetic HTTP scripts.


Meteor Shower
Photo by Austin Schmid on Unsplash

Performance testing is often treated as a completely separate discipline from functional testing. Teams write end-to-end tests with Playwright, then later build a second set of HTTP scripts in another framework for load testing.


The problem is that HTTP scripts only simulate requests. They don't execute your frontend application, measure browser rendering, or reproduce the JavaScript executed by real users.

Modern SPAs built with React, Next.js, Vue or Angular are no longer simple collections of API endpoints. A single page load may trigger dozens of API requests, background fetches, authentication flows, client-side caching, and dynamic rendering.


This is exactly where Artillery's Playwright engine becomes interesting.


Instead of emulating HTTP traffic, Artillery executes real Playwright browser code https://www.artillery.io/docs/playwright under concurrent load while automatically collecting Core Web Vitals such as LCP (Largest Contentful Paint), FCP (First Contentful Paint), CLS (Cumulative Layout Shift) and INP (Interaction to Next Paint).



Why Browser-Based Load Testing?

Traditional load tests measure backend capacity. Browser-based load tests measure user experience.


While API load tests tell you how many requests your infrastructure can process, they don't reveal how your application behaves once JavaScript executes, routes change, API calls are combined, and pages render in the browser.


Browser-based testing evaluates complete user journeys under concurrent load, helping you identify whether performance issues originate in the frontend, the backend, or the interaction between the two.


Typical questions include:

  • How quickly does the application become interactive?

  • Which business workflow degrades first under load?

  • Does the frontend remain responsive as concurrency increases?

  • Are users still able to complete critical tasks successfully?


Architecture Overview


Performance testing workflow architecture.

The performance testing workflow begins inside a Docker container running the Artillery CLI. Artillery creates multiple virtual users and orchestrates concurrent browser sessions using Playwright. Unlike traditional HTTP load testing, each virtual user executes the same browser interactions as a real user, including JavaScript execution, routing, rendering, and network requests.


The Playwright browser interacts with your Single Page Application (SPA), whether it's built with React, Next.js, Vue, or Angular. Because the application runs in a real browser, every request is generated naturally by the frontend instead of being manually scripted.


The SPA communicates with Supabase through the official JavaScript SDK, handling authentication, database queries, storage operations, and Edge Function calls. If distributed tracing is enabled (tracePropagation: true), the SDK propagates the active trace context (traceparent) with each request.


Finally, Supabase receives the requests and records corresponding API, authentication, database, storage, and Edge Function logs. By correlating these logs with the browser performance metrics collected by Artillery, developers can investigate not only when a user journey becomes slower under load, but also which backend operation or service is responsible for the degradation.


This architecture enables a complete end-to-end performance investigation using existing Playwright browser automation, without requiring synthetic HTTP scripts or complex performance testing infrastructure.


Running Your First Browser Load Test

One of the advantages of Artillery is that you don't need to install Node.js packages globally or configure browsers on your machine. The official Docker image contains everything required to execute browser-based load tests.


Create a new file called load-test.ts:


import { Config, Scenario } from "artillery";
export const config: Config = {

  target: "https://your-app.com",
  phases: [
    {
      duration: 60,
      arrivalRate: 5
    }
  ],
  engines: {
    playwright: {}
  }
};

export const scenarios: Scenario[] = [
  {
    engine: "playwright",
    testFunction: homePage
  }
];

async function homePage(page) {
  await page.goto("/");
}

This example configures Artillery to:

  • launch real browsers,

  • create five new virtual users every second,

  • execute the homePage browser journey for sixty seconds,

  • automatically collect browser performance metrics throughout the test.


Unlike HTTP-based load testing, every virtual user executes JavaScript, performs rendering, and interacts with your application exactly like a real visitor.


Running with Docker

Assuming your load-test.ts file is located in the current directory, execute:


$ docker run --rm \
  -v $(pwd):/scripts \
  artilleryio/artillery:latest \
  run /scripts/load-test.ts

The Docker container starts Artillery, executes the configured browser scenario, prints a summary to the terminal, and exits automatically once the test completes.


Because your project directory is mounted into the container, you can edit the test locally while continuing to execute it inside Docker without rebuilding any images.


Artillery documentation about using docker https://www.artillery.io/docs/docker 


Understanding the Test Configuration

The configuration contains three important concepts.


Target

This is the base URL that Playwright will use when navigating to relative paths such as:


await page.goto("/");

Load Profile


phases: [
  {
    duration: 60,
    arrivalRate: 5
  }
]

This tells Artillery to create five new virtual users every second for one minute.

It's important to understand that arrivalRate does not equal concurrent users. If each user journey takes several seconds to complete, multiple browser sessions will be active simultaneously.


Browser Scenario


export const scenarios = [
  {
    engine: "playwright",
    testFunction: homePage
  }
];

Unlike Playwright Test, Artillery executes a plain JavaScript function.

That function receives a Playwright Page object and performs the browser automation for a single virtual user.


Using Existing Playwright Code

If you already maintain Playwright E2E tests, you don't have to start from scratch.

Artillery doesn't execute Playwright Test suites (test() and test.describe()), but it can reuse the same browser automation logic by importing shared functions, page objects, or helper modules.


For example, instead of embedding browser interactions directly inside your tests:


test("User Login", async ({ page }) => {
    await login(page);
});

extract the business flow into a reusable function:


export async function login(page) {
    await page.goto("/");
    ...
}

The same function can then be used by both Playwright Test and Artillery:


export const scenarios = [{
    engine: "playwright",
    testFunction: login
}];

This approach keeps your functional and performance tests aligned while avoiding duplicated browser automation.


Correlating Performance with Supabase Traces

Running a browser load test tells you when performance starts to degrade. For example, you may observe that the checkout journey's LCP increases from 1.2 seconds to 4.8 seconds once 100 virtual users are active.


However, browser metrics alone cannot explain why the application became slower.


Was the slowdown caused by:

  • a slow PostgreSQL query?

  • an Edge Function?

  • authentication?

  • storage operations?

  • or simply increased frontend rendering time?


To answer these questions, we need to correlate browser activity with backend telemetry.

If your application uses the Supabase JavaScript client, enable distributed trace propagation when creating the client:


const supabase = createClient(url, anonKey, {
  tracePropagation: true
});

Supabase official documentation for enabling tracing https://supabase.com/docs/guides/telemetry/client-side-tracing 



Performance testing workflow with Supabase Tracing

When an active OpenTelemetry trace exists, the Supabase client automatically propagates the standard W3C traceparent header with every request.


This allows requests originating from your browser session to be correlated with Supabase platform logs.


Now imagine your Artillery report shows that the Generate Report journey suddenly becomes slow under load.


Instead of only seeing P95 = 6.4 s (95% of all executed user journeys completed in 6.4 seconds or less).





You can correlate that browser session with Supabase logs and discover that:

  • authentication completed normally,

  • the Edge Function executed for 5.7 seconds,

  • the function spent 5.4 seconds waiting on a PostgreSQL query.


At that point, you've moved beyond identifying a performance problem. You've found its likely root cause.


This combination of browser metrics from Artillery and backend telemetry from Supabase provides a much more complete picture than either tool alone. Rather than knowing only that users experienced a slowdown, you can follow a request from the browser through authentication, database access, storage, and Edge Functions to understand exactly where time is being spent.

 
 
 

Comments


bottom of page