How to Speed Up Selenium Python Headless Chrome on macOS
Speed up Selenium headless Chrome on macOS with page-load strategies, explicit waits, Selenium Manager, timeouts, and repeatable benchmarks.
Use Chrome’s modern headless mode, choose the least blocking page-load strategy that preserves correctness, replace fixed sleeps with explicit waits, and keep browser setup current. On macOS, Selenium Manager can discover a compatible ChromeDriver automatically, but it does not make page execution faster by itself. Measure the pages and assertions that matter instead of relying on a universal percentage improvement.
1. A fast, reliable baseline
Install or upgrade Selenium, then launch Chrome with --headless=new. The Chrome documentation describes headless mode as running without a visible user interface. Selenium’s current Python guidance uses Selenium Manager, so a separate webdriver-manager package is usually unnecessary.
python3 -m pip install --upgrade selenium
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
options = Options()
options.add_argument("--headless=new")
options.add_argument("--window-size=1440,900")
options.page_load_strategy = "eager"
driver = webdriver.Chrome(options=options) # Selenium Manager handles driver discovery
driver.set_page_load_timeout(30)
driver.set_script_timeout(30)
try:
driver.get("https://example.com")
print(driver.title)
finally:
driver.quit()
eager is a useful default when your assertion needs the DOM before images, stylesheets, and other subresources finish. Use normal when the test contract requires a fully loaded page. Use none only when your script has a reliable application-specific readiness check, because navigation can return before WebDriver knows that the page is usable. Selenium defines these three strategies in its Python options API.
2. Choose the right page-load strategy
| Strategy | Navigation behavior | Use it when | Main risk |
|---|---|---|---|
normal |
Waits for all resources | Your test depends on complete resource loading, downloads, or final page state | Slow images, analytics, ads, and third-party requests extend get() |
eager |
Returns when the DOM is ready while other resources may still load | Assertions need rendered structure or application data loaded during DOM work | An image, font, or late request may not be ready yet |
none |
Does not block WebDriver on navigation completion | You can wait on a precise application condition yourself | Any immediate interaction can race the page and become flaky |
Using none safely
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
options = Options()
options.add_argument("--headless=new")
options.page_load_strategy = "none"
driver = webdriver.Chrome(options=options)
try:
driver.get("https://example.com/dashboard")
WebDriverWait(driver, 30).until(
EC.visibility_of_element_located((By.CSS_SELECTOR, "[data-testid='dashboard-ready']"))
)
print(driver.find_element(By.TAG_NAME, "h1").text)
finally:
driver.quit()
The readiness selector must represent the state your assertion needs. A generic document.readyState check is not sufficient for a single-page application that fetches data after the initial document.
3. Replace fixed sleeps with explicit waits
time.sleep() always waits the full duration, even when the page is already ready. Explicit waits poll until a condition succeeds or a bounded timeout expires.
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
wait = WebDriverWait(driver, 20, poll_frequency=0.1)
button = wait.until(EC.element_to_be_clickable((By.CSS_SELECTOR, "button.submit")))
button.click()
wait.until(EC.visibility_of_element_located((By.CSS_SELECTOR, ".result")))
Wait for the condition you actually need
- Element exists:
presence_of_element_located. - Element can be seen:
visibility_of_element_located. - Element can be used:
element_to_be_clickable. - URL transition:
url_containsorurl_to_be. - Application state: a lambda that reads a status element, JavaScript property, or result count.
wait.until(lambda d: d.find_element(By.CSS_SELECTOR, "#status").text == "Complete")
4. Reduce work without breaking the test
- Open only the pages, tabs, and flows required by the assertion.
- Do not enable extensions, downloads, screenshots, or extra logging unless the test uses them.
- Reuse one browser session for related cases when isolation permits it. Startup and teardown have a cost; clear cookies, local storage, and application state between cases.
- Use stable selectors such as
data-testidinstead of expensive or fragile XPath expressions. - Block ads, trackers, or unused resource types only when the application behavior under test does not depend on them. Removing a required script creates false results.
- Keep the browser and driver versions aligned. Selenium Manager can discover and manage the matching toolchain on macOS, reducing setup and mismatch failures.
5. Set bounded timeouts
Timeouts do not make a server respond faster. They define how long a test is willing to wait before recovering or failing, which prevents one pathological navigation from holding a suite indefinitely. Selenium exposes page-load and asynchronous script timeouts in its Python WebDriver API.
driver.set_page_load_timeout(30)
driver.set_script_timeout(30)
driver.implicitly_wait(0) # keep synchronization explicit
Use a longer page-load timeout for known slow environments, but keep explicit waits close to the condition they protect. Avoid mixing a large implicit wait with explicit waits because the two polling systems can compound delays.
6. macOS setup and repeatable benchmarking
Benchmark the same URLs, test data, browser version, Selenium version, Python version, Mac architecture, and network conditions. Record three timings: driver startup, navigation-to-ready, and complete test flow.
from time import perf_counter
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
options = Options()
options.add_argument("--headless=new")
options.add_argument("--window-size=1440,900")
options.page_load_strategy = "eager"
startup_start = perf_counter()
driver = webdriver.Chrome(options=options)
startup_seconds = perf_counter() - startup_start
driver.set_page_load_timeout(30)
try:
navigation_start = perf_counter()
driver.get("https://example.com")
WebDriverWait(driver, 20).until(
EC.presence_of_element_located((By.TAG_NAME, "h1"))
)
ready_seconds = perf_counter() - navigation_start
print({
"startup_seconds": round(startup_seconds, 3),
"navigation_to_ready_seconds": round(ready_seconds, 3),
"title": driver.title,
})
finally:
driver.quit()
Run each configuration several times and compare medians, not the fastest single run. There is no authoritative universal percentage improvement for this exact macOS workflow; any reported percentage should include the page, versions, hardware, and methodology.
7. Common errors and fixes
| Error or symptom | Likely cause | Fix |
|---|---|---|
SessionNotCreatedException |
Chrome and driver versions are incompatible | Upgrade Selenium and let Selenium Manager resolve the driver; verify the installed Chrome version. |
selenium.common.exceptions.TimeoutException from get() |
The server or a subresource exceeded the page-load timeout | Use eager if complete loading is unnecessary, increase the bounded timeout for the environment, or investigate the slow request. |
Element is not found after none |
Navigation returned before the application rendered the element | Add an explicit wait for the application’s ready marker or use eager. |
| Flaky clicks or stale elements | The DOM changed between lookup and interaction | Wait for clickability, locate the element immediately before using it, and avoid arbitrary sleeps. |
| Headless layout differs from local Chrome | Viewport, fonts, device scale, or responsive breakpoints differ | Set an explicit window size, install required fonts, and compare the same browser version and architecture. |
| Driver startup is slow | Repeated process startup or driver discovery | Reuse a session for related cases and keep Selenium Manager and browser installations available locally. |
| Script hangs indefinitely | An asynchronous script or wait has no bound | Set script, page-load, and explicit-wait timeouts. |
8. Reliability, performance, and cost considerations
A faster get() call is useful only if the next assertion has a reliable readiness condition. Treat page-load strategy, waits, and resource blocking as one synchronization design. Keep a slower normal path for tests that verify complete loading, and use a focused eager or none path for DOM-level checks.
For large screenshot workloads, running a full browser for every URL adds process, navigation, and cleanup overhead. A screenshot API can make that workflow smaller and easier to price because capture, retries, and output handling are provided as a request.
Or skip the browser setup
ScreenshotNeo provides a website screenshot API and MCP server. One GET request returns PNG, JPEG, WebP, or PDF output. It accepts cookie and consent banners before capture and removes more than 60 known consent platforms, newsletter popups, and chat widgets; each step can be turned off. Bot checks, CAPTCHAs, blank pages, timeouts, failed loads, and cache hits are not billed, and response headers identify the page verdict and billing status.
See the ScreenshotNeo API documentation for the full option set, including full-page capture, CSS-element capture, dark mode, device presets, retina scale, PDF settings, custom CSS and JavaScript, clicks, waits, request blocking, headers, cookies, user agents, authorization, timezone, geolocation, transparent backgrounds, resizing, TTL caching, signed links, asynchronous jobs, webhooks, bulk capture, usage data, and the OpenAPI specification.
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
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)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' }); const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
ScreenshotNeo also has an MCP server with take_screenshot, get_page_info, and capture_pdf tools for Claude, Cursor, and other MCP clients. The Free plan includes 1,000 shots per month with no card; paid plans start at $5 for 3,000 shots. Create a free ScreenshotNeo account.
FAQ
Is --headless=new faster than headed Chrome on every Mac?
It removes visible UI work, but the result depends on the page, browser version, Mac hardware, and test flow. Measure your representative workload.
Should every test use none?
No. Use it only with a reliable readiness condition. Otherwise, eager is usually a safer speed and correctness compromise.
Do I still need webdriver-manager?
Usually not. Current Selenium includes Selenium Manager for browser and driver discovery. It reduces setup maintenance but does not optimize page execution.
Can a timeout fix a slow website?
No. It bounds how long your test waits and lets it fail or recover predictably; the server still determines response time.


