> ## Documentation Index
> Fetch the complete documentation index at: https://codspeed.io/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Profiling apps with Playwright

> Drive an app through Playwright and report measured user flows to CodSpeed.

export const CIWorkflow = ({minimal = false, enableWorkflowDispatch = true, runsOn = "ubuntu-latest", highlight = [], mode, modes, submodules = false, preSteps = [], buildSteps = ["# ...", "# Setup your environment here:", "#  - Configure your Python/Rust/Node version", "#  - Install your dependencies", "#  - Build your benchmarks (if using a compiled language)", "# ..."], benchmarkCommand = ["<Insert your benchmark command here>"], jobName = "Run benchmarks", env = {}}) => {
  const modeList = modes || (mode ? [mode] : undefined);
  if (!modeList || modeList.length === 0) {
    throw new Error("mode or modes is required");
  }
  const indent = (lines, depth) => {
    const reindentedLines = lines.map(l => l.length === 0 ? l : (" ").repeat(depth) + l);
    return reindentedLines.join("\n");
  };
  const workflowDispatchSection = enableWorkflowDispatch ? "  # `workflow_dispatch` allows CodSpeed to trigger backtest\n" + "  # performance analysis in order to generate initial data.\n" + "  workflow_dispatch:\n" : "";
  let yaml = "";
  if (!minimal) {
    yaml += `
name: CodSpeed Benchmarks

on:
  push:
    branches:
      - "main" # or "master"
  pull_request:
`;
    yaml += workflowDispatchSection;
  }
  yaml += `
jobs:
  benchmarks:
    name: ${jobName}
    runs-on: ${runsOn}`;
  if (!minimal) {
    yaml += `
    permissions: # optional for public repositories
      contents: read # required for actions/checkout
      id-token: write # required for OIDC authentication with CodSpeed`;
  }
  if (preSteps.length > 0) yaml += "\n" + indent(preSteps, 4);
  yaml += `
    steps:
      - uses: actions/checkout@v5`;
  if (submodules) {
    const value = typeof submodules === "string" ? submodules : "true";
    yaml += `\n        with:\n          submodules: ${value}`;
  }
  yaml += "\n" + indent(buildSteps, 6);
  const modeValue = modeList.join(",");
  yaml += `
      - name: Run the benchmarks
        uses: CodSpeedHQ/action@v4
        with:
          mode: ${modeValue}`;
  if (benchmarkCommand.length > 0) {
    const indentedBenchCommand = benchmarkCommand.length > 1 ? benchmarkCommand[0] + "\n" + indent(benchmarkCommand.slice(1), 12) : benchmarkCommand;
    const runLine = indent(["run: "], 10) + indentedBenchCommand;
    yaml += `\n${runLine}`;
  }
  const envEntries = Object.entries(env);
  if (envEntries.length > 0) {
    const envLines = ["env:", ...envEntries.map(([k, v]) => `  ${k}: ${v}`)];
    yaml += "\n" + indent(envLines, 8);
  }
  return <CodeBlock language="yaml" highlight={JSON.stringify(highlight)} {...minimal || ({
    filename: ".github/workflows/codspeed.yml",
    icon: "github"
  })}>
      {yaml}
    </CodeBlock>;
};

export const NpmInstall = ({packages}) => {
  return <CodeGroup>
      <code className="language-sh" filename="npm">
      npm install --save-dev {packages}
      </code>

      <code className="language-sh" filename="yarn">
      yarn add --dev {packages}
      </code>

      <code className="language-sh" filename="pnpm">
      pnpm add -D {packages}
      </code>
    </CodeGroup>;
};

<Note>
  The `@codspeed/playwright-plugin` integration currently supports only the
  [walltime instrument](/instruments/walltime). CPU Simulation is not available.
</Note>

[`@codspeed/playwright-plugin`](https://www.npmjs.com/package/@codspeed/playwright-plugin)
is the CodSpeed integration for [Playwright](https://playwright.dev). It runs a
user-defined flow against a target application, measures the time spent inside
that flow, and reports it to CodSpeed. The flow itself is plain Playwright code,
so anything Playwright can drive can be benchmarked.

<Info>
  Today the plugin supports [Electron](https://www.electronjs.org) apps as a
  target. Browser-based targets (existing dev servers, static builds, hosted URLs)
  are on the roadmap and will be added under the same `bench` API.
</Info>

## Installation

Install the plugin alongside `playwright`:

<NpmInstall packages="@codspeed/playwright-plugin playwright" />

## Example usage with Electron

Build your Electron app first so the main entrypoint exists (e.g.,
`out/main/index.js`), then declare a benchmark with `target.kind` set to
`"electron"`:

```ts bench/inbox.bench.ts theme={null}
import { bench } from "@codspeed/playwright-plugin";
import path from "node:path";

bench(
  "inbox-search",
  async ({ page }) => {
    await page.fill("#search", "quarterly report");
    await page.waitForSelector("#results");
  },
  {
    target: {
      kind: "electron",
      appPath: path.resolve("out/main/index.js"),
    },
    beforeRound: async ({ page }) => {
      await page.waitForSelector("#main:not(.loading)");
    },
    rounds: 5,
  }
);
```

For each round, the plugin launches Electron with the provided main entrypoint,
waits for the first window, runs `beforeRound`, measures `fn`, runs
`afterRound`, then closes the app.

For a more complete example, have a look at the
[example benchmark](https://github.com/CodSpeedHQ/codspeed-node/tree/main/examples/with-electron-and-walltime)
included in the codspeed-node repository.

## API

The plugin exposes a single `bench` function. Its shape is target-agnostic:

```ts theme={null}
import { bench } from "@codspeed/playwright-plugin";

bench(name, fn, options);
```

<ParamField path="name" type="string" required>
  Identifier of the benchmark, used by CodSpeed to track it across runs.
</ParamField>

<ParamField path="fn" type="({ page }) => void | Promise<void>" required>
  The function whose execution time is measured. Receives a Playwright
  [`Page`](https://playwright.dev/docs/api/class-page) bound to the target.
  Everything that runs inside `fn` counts toward the reported timing.
</ParamField>

<ParamField path="options" type="BenchOptions" required>
  Target configuration and benchmark settings, detailed below.
</ParamField>

### Options

<ParamField path="target" type="Target" required>
  Discriminated union describing what to drive. The `kind` field selects the
  target; the remaining fields are specific to that kind. Current variants:
  [`{ kind: "electron", ... }`](#electron-target-options). More will be added
  without breaking existing call sites.
</ParamField>

<ParamField path="rounds" type="number" default="1">
  Number of measurement rounds. Can be overridden at runtime via the
  `CODSPEED_ROUNDS` environment variable.
</ParamField>

<ParamField path="beforeRound" type="({ page }) => void | Promise<void>">
  Runs before each round, after the target is ready. Use it to bring the app to
  a ready state. Not measured.
</ParamField>

<ParamField path="afterRound" type="({ page }) => void | Promise<void>">
  Runs after each round, before the target is torn down. Not measured.
</ParamField>

### Electron target options

<ParamField path="target.kind" type="&#x22;electron&#x22;" required>
  Selects the Electron target.
</ParamField>

<ParamField path="target.appPath" type="string" required>
  Absolute path to the Electron main entrypoint, e.g., `out/main/index.js`.
</ParamField>

<ParamField path="target.electronArgs" type="string[]" default="[]">
  Extra CLI flags forwarded to the Electron process.
</ParamField>

<ParamField path="target.cwd" type="string" default="process.cwd()">
  Working directory for the Electron process. Also the directory `electron` is
  resolved from when `target.electronExecutablePath` is not set.
</ParamField>

<ParamField path="target.electronExecutablePath" type="string">
  Absolute path to the Electron binary. Only set this to override the default
  resolution.
</ParamField>

## Running the benchmarks locally

With node 24+, you can run typescript files directly:

```shellsession title=terminal icon="square-terminal" theme={null}
$ node bench/inbox.bench.ts
[CodSpeed] [round 1/5] 42.13 ms
[CodSpeed] [round 2/5] 41.78 ms
[CodSpeed] [round 3/5] 42.05 ms
[CodSpeed] [round 4/5] 41.92 ms
[CodSpeed] [round 5/5] 42.21 ms
```

Locally, `bench` runs the app and prints per-round timings to the terminal.
Results are uploaded to CodSpeed only when running in the
[CI environment](#running-the-benchmarks-in-your-ci) or when using the
[CodSpeed CLI](/cli#running-benchmarks).

## Running the benchmarks in your CI

To generate performance reports, you need to run the benchmarks in your CI. This
allows CodSpeed to automatically run benchmarks and warn you about regressions
during development.

<Tip>
  If you want more details on how to configure the CodSpeed action, you can check
  out the [Continuous Reporting section](/integrations/ci).
</Tip>

Here is an example of a GitHub Actions workflow that runs the benchmarks and
reports the results to CodSpeed on every push to the `main` branch and every
pull request:

<CIWorkflow
  mode="walltime"
  runsOn="codspeed-macro"
  buildSteps={[
"- uses: actions/setup-node@v6",
"- name: Install dependencies",
"  run: npm install",
"- name: Build the Electron app",
"  run: npm run build",
]}
  benchmarkCommand={["node bench/inbox.bench.ts"]}
  env={{ CODSPEED_WALLTIME_PROFILER: "samply" }}
/>

<Warning>
  Two pieces of this workflow are not optional:

  * The job must run on a [CodSpeed Macro runner](/features/macro-runners)
    (`runs-on: codspeed-macro`). Walltime measurements are not stable on shared
    GitHub-hosted runners.
  * `CODSPEED_WALLTIME_PROFILER` must be set to `samply` on the benchmark step.
    The plugin currently only supports the
    [`samply`](https://github.com/mstange/samply) profiler; without it, the run
    will fail to produce [profiling information](/features/profiling).
</Warning>

<Tip>
  Electron needs a display to render its window. On headless CI runners, wrap the
  benchmark command with
  [`xvfb-run`](https://www.x.org/releases/X11R7.6/doc/man/man1/Xvfb.1.xhtml) or
  install a virtual framebuffer, otherwise the app will fail to start.
</Tip>
