ScreenshotNeo

BlogHow-to

How to Use Selenium for Performance Testing

Learn where Selenium fits in performance testing, how to measure real browser journeys, and when to use JMeter for concurrency and load.

By the ScreenshotNeo team1 October 20269 min read

Short answer: Selenium can measure a small number of realistic browser journeys and collect browser-visible diagnostics. It is not a dedicated high-concurrency load generator. Use Selenium to answer questions about user experience, rendering, network failures, and JavaScript errors; use Apache JMeter or another protocol-level tool to generate controlled concurrency and measure server capacity. Keep a small Selenium cohort running during the load test to confirm that real browser journeys still work.

Selenium WebDriver drives a real browser through a browser-specific driver. A test framework supplies assertions, lifecycle, and reporting; WebDriver itself does not. Selenium’s project describes its scope simply: “Selenium automates browsers. That’s it!” Selenium project

1. Decide what you are testing

Question Best fit Why
Can a user sign in, search, or complete checkout? Selenium Executes the real browser journey.
How long does a key browser-visible step take? Selenium Measures navigation, waits, rendering, and browser events.
Which request, console message, or JavaScript error accompanies a slowdown? Selenium with WebDriver BiDi BiDi can stream network, console, script, and browser events.
How many concurrent users can the service handle? JMeter or another protocol tool Creates many lightweight virtual users without browser startup and rendering overhead.
Does the site work in several browsers at once? Selenium Grid or RemoteWebDriver Distributes browser sessions across machines.

The Selenium project advises that performance testing with Selenium and WebDriver is generally not advised because browser startup, HTTP-server behavior, third-party resources, and WebDriver instrumentation can vary independently of the application under test. Selenium test-practice guidance

2. Build a controlled Selenium performance check

Define the journey and environment

  1. Choose one small, stable path such as sign-in, search, checkout, or a dashboard view.
  2. Record browser and version, driver version, operating system, viewport, location, network conditions, test-data identifiers, and build or commit ID.
  3. Use dedicated test data and avoid changing application state between repetitions unless the journey requires it.
  4. Separate setup, the measured journey, and teardown. Do not include driver startup in the page-step timing unless startup is the thing you are studying.

Install the Python binding

python -m venv .venv
. .venv/bin/activate
pip install selenium

Install a supported browser. Selenium Manager can resolve drivers for many local setups; in controlled CI environments, pin the browser and driver versions when repeatability matters.

Runnable Python example

from __future__ import annotations

import json
import statistics
import time
from dataclasses import asdict, dataclass
from datetime import datetime, timezone

from selenium import webdriver
from selenium.common.exceptions import TimeoutException
from selenium.webdriver.common.by import By
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.support.ui import WebDriverWait

URL = "https://example.com/"
RUNS = 5

@dataclass
class RunResult:
    run: int
    navigation_ms: float | None
    ready_ms: float | None
    title: str | None
    error: str | None


def make_driver() -> webdriver.Chrome:
    options = webdriver.ChromeOptions()
    options.add_argument("--headless=new")
    options.add_argument("--window-size=1440,900")
    options.set_capability("goog:loggingPrefs", {"browser": "ALL"})
    return webdriver.Chrome(options=options)


def one_run(run_number: int) -> RunResult:
    driver = make_driver()
    wait = WebDriverWait(driver, 20)
    started = time.perf_counter()
    try:
        driver.get(URL)
        navigation_ms = (time.perf_counter() - started) * 1000
        wait.until(EC.presence_of_element_located((By.TAG_NAME, "body")))
        ready_ms = (time.perf_counter() - started) * 1000
        return RunResult(run_number, navigation_ms, ready_ms, driver.title, None)
    except TimeoutException as exc:
        return RunResult(run_number, None, None, None, f"timeout: {exc}")
    except Exception as exc:
        return RunResult(run_number, None, None, None, repr(exc))
    finally:
        try:
            print(json.dumps({"run": run_number, "console": driver.get_log("browser")}))
        except Exception:
            pass
        driver.quit()


results = [one_run(i) for i in range(1, RUNS + 1)]
values = [r.ready_ms for r in results if r.ready_ms is not None]
summary = {
    "captured_at": datetime.now(timezone.utc).isoformat(),
    "url": URL,
    "runs": [asdict(r) for r in results],
    "median_ready_ms": statistics.median(values) if values else None,
    "min_ready_ms": min(values) if values else None,
    "max_ready_ms": max(values) if values else None,
}
print(json.dumps(summary, indent=2))

Replace the URL and readiness condition with a real journey. For a sign-in flow, wait for a post-login element rather than an arbitrary sleep. Keep raw timings and environment metadata so a later comparison can explain variation.

Measure browser timings from JavaScript

timings = driver.execute_script("""
const n = performance.getEntriesByType('navigation')[0];
return n ? {
  dns_ms: n.domainLookupEnd - n.domainLookupStart,
  connect_ms: n.connectEnd - n.connectStart,
  ttfb_ms: n.responseStart - n.requestStart,
  dom_content_loaded_ms: n.domContentLoadedEventEnd - n.startTime,
  load_event_ms: n.loadEventEnd - n.startTime
} : null;
""")
print(timings)

Navigation timing is page evidence, not a complete server benchmark. Third-party scripts, cache state, CPU contention, and the browser version all affect it.

Node.js example

import { Builder, By, until } from "selenium-webdriver";

const driver = await new Builder().forBrowser("chrome").build();
try {
  const start = performance.now();
  await driver.get("https://example.com/");
  await driver.wait(until.elementLocated(By.css("body")), 20_000);
  console.log({ readyMs: performance.now() - start, title: await driver.getTitle() });
} finally {
  await driver.quit();
}

3. Use explicit waits and stable measurements

Explicit waits express readiness conditions and avoid timing your test against an arbitrary delay. Useful conditions include an element becoming visible, a URL changing, a loading indicator disappearing, or a known application state appearing. Keep the timeout consistent and record timeout failures separately from successful timings.

wait.until(EC.visibility_of_element_located((By.CSS_SELECTOR, "[data-testid='results']")))
wait.until(EC.url_contains("/dashboard"))

Warm up the environment before collecting baseline samples. Run enough repetitions to see variation, retain every raw observation, and compare distributions or percentiles with a baseline. Do not treat one browser run as a universal performance number.

4. Capture richer diagnostics with WebDriver BiDi

WebDriver BiDi uses a WebSocket connection for asynchronous browser events. Selenium documents it as the standards-based direction for events that include network requests, console messages, JavaScript errors, and browser or script activity. Implementation coverage is still evolving, so check the binding and browser support for the events you need. Selenium WebDriver BiDi documentation

Use BiDi or browser logging to associate a slow step with evidence such as a failed request, a console error, or an unusually long resource. Store the event stream with the timing result and build identifier.

5. Add controlled load with JMeter

Apache JMeter is an open-source Java application for load testing and performance measurement across web, API, database, messaging, FTP, and other protocols. Apache JMeter can generate concurrent protocol traffic while Selenium checks a smaller representative browser cohort.

  1. Model the HTTP requests, authentication, variables, and assertions in JMeter.
  2. Start with a documented user count and ramp-up schedule.
  3. Capture response time distributions, throughput, error rate, and server-side resource data.
  4. Run Selenium journeys during the same load window and record browser-visible failures.
  5. Compare unloaded and loaded browser results using the same browser, viewport, data, and location.

Simple cURL request timing

curl -sS -o /dev/null \
  -w 'dns=%{time_namelookup}s connect=%{time_connect}s ttfb=%{time_starttransfer}s total=%{time_total}s\n' \
  https://example.com/

cURL is useful for a quick protocol-level spot check. It does not render the page or execute its JavaScript, so it cannot replace Selenium for browser experience.

6. Run browser checks in parallel with Grid

Selenium Grid and RemoteWebDriver place sessions on remote machines and support parallel cross-browser execution. Grid improves browser-check coverage and throughput; distributing browsers does not remove browser startup, rendering, third-party-resource, or WebDriver instrumentation variability, and it is not a substitute for a lightweight load generator.

from selenium import webdriver
from selenium.webdriver.chrome.options import Options

options = Options()
options.add_argument("--headless=new")
driver = webdriver.Remote(
    command_executor="http://grid-host:4444",
    options=options,
)
try:
    driver.get("https://example.com/")
    print(driver.title)
finally:
    driver.quit()

7. What to record in every result

Area Record
Journey Steps, readiness conditions, test-data state, pass/fail outcome.
Timing Step duration, navigation timing, resource timing, and raw samples.
Browser Browser and driver versions, operating system, viewport, device emulation if used.
Environment Geography, network conditions, CPU and memory contention, build identifier.
Diagnostics Network failures, console errors, JavaScript exceptions, screenshots or traces when needed.
Load Virtual users, ramp-up, throughput, response percentiles, error rate, and server metrics.

8. Common errors and fixes

Error or symptom Likely cause Fix
SessionNotCreatedException Browser and driver mismatch or unsupported flags. Pin compatible versions, update Selenium, and inspect the driver log.
TimeoutException The readiness condition never occurred, the page is slow, or test data is invalid. Capture URL, console and network evidence; verify the selector; use an explicit, documented timeout.
Flaky timings Cold caches, shared CPU, third-party resources, or inconsistent test data. Warm up, isolate workers, control data and environment, and compare distributions.
Element is present but not usable It is hidden, covered, detached, or still changing. Wait for visibility or clickability and wait for the application state that makes the element stable.
Parallel tests interfere Shared accounts, files, carts, or backend records. Give each worker isolated data and independent browser profiles.
Grid session fails Remote endpoint, capabilities, capacity, or network problem. Check the Grid status, endpoint URL, browser capabilities, node capacity, and remote logs.
Load test looks healthy but users see errors Protocol traffic does not execute browser JavaScript or rendering. Keep Selenium journeys running during the load test and inspect browser events.

9. Performance, reliability, and cost notes

  • Performance: Browser sessions consume substantially more CPU, memory, and startup time than protocol virtual users. Keep Selenium concurrency small and intentional.
  • Reliability: Pin browser, driver, operating-system image, viewport, location, and test data. Preserve raw samples and environment metadata.
  • Third parties: Ads, analytics, chat, fonts, and external APIs can dominate browser timings. Decide whether to include them as part of the user experience or isolate them for an application-focused test.
  • Statistics: Report median and tail behavior from repeated runs. A single “page-load time” hides variation.
  • Cost: Selenium software is free, but CI workers, hosted Grid capacity, browsers, and observability storage can add cost. Protocol load generation is usually more resource-efficient for high concurrency.

10. Or skip the browser setup

If you need screenshots for visual checks, documentation, or a lightweight browser capture rather than a full Selenium journey, ScreenshotNeo provides a website screenshot API and MCP server. Cookie and consent banners are accepted and removed before capture, along with more than 60 known consent platforms, newsletter popups, and chat widgets. Bot checks, blank pages, timeouts, failed loads, and cache hits are not billed, and each response reports the page verdict and billing status.

One request returns PNG, JPEG, WebP, or PDF. The API supports full-page capture with lazy images, CSS-selector element capture, dark mode, device presets and custom viewports, retina scale, PDF settings, custom CSS and JavaScript, clicks, selector or network-idle waits, request blocking, headers, cookies, user agents, authorization, timezone, geolocation, transparent backgrounds, resizing, TTL caching, signed image links, asynchronous jobs with signed webhooks, bulk capture of up to 100 URLs per call, usage reporting, and an OpenAPI specification. An MCP server provides take_screenshot, get_page_info, and capture_pdf for Claude, Cursor, and other MCP clients.

cURL (API documentation):

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp

Python:

import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
open("shot.webp", "wb").write(r.content)

Node.js:

const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);

There is a free plan with 1,000 screenshots per month and no card. Paid plans start at $5 for 3,000 shots; every feature is available on every plan. Create a free ScreenshotNeo account.

FAQ

Can Selenium test concurrent users?

It can start concurrent browser sessions, but it is inefficient for measuring large-scale concurrency. Use JMeter or a comparable protocol-level generator for load, with a smaller Selenium cohort for browser validation.

Is Selenium good for performance testing?

It is useful for browser experience and diagnostics. It is generally not advised as the primary tool for controlled server performance testing because browser and WebDriver variables can contaminate measurements.

How do I measure page-load time with Selenium?

Start a monotonic timer before navigation, wait for a defined readiness condition, and collect Navigation Timing entries. Report repeated samples with the browser and environment metadata.

Should I use Selenium or JMeter?

Use Selenium for real browser journeys and JMeter for protocol-level concurrency, throughput, and server-capacity questions. A combined test often gives the clearest result.

Does Selenium Grid make load testing accurate?

Grid distributes browser sessions and helps cross-browser coverage. It does not turn browser sessions into lightweight virtual users or remove rendering and instrumentation variability.