Home / Blog / Playwright vs WebdriverIO
Playwright vs WebdriverIO: assembled, or assembling?
WebdriverIO 9 and Playwright 1.62 are both maintained test runners. WebdriverIO speaks W3C WebDriver and WebDriver BiDi, and reaches native mobile apps through Appium. Playwright ships the runner, the assertions and three engines in one package, and cannot test a native app at all. Web-only estate: Playwright is less to own. Native apps in the suite: stay.
We work in Playwright and we sell Playwright migrations, so weigh this page accordingly. Both suites below were installed and run before this page went up, on Windows 11 and Node 20.19.6: WebdriverIO 9.31.5 driving the Chrome 152 already on the machine, and Playwright 1.62.1 across its three engines. Nothing here will tell you WebdriverIO was a mistake, and nothing here pretends the mobile half of a suite can move.
What is the difference between Playwright and WebdriverIO?
Both are test runners that live in Node and drive a browser from outside it. You point a config file at a directory of specs, the runner starts a worker, the worker opens a browser, and the assertions run in your Node process. Nothing in that description separates them.
The samples on this page were written against Playwright 1.62.1 and WebdriverIO 9.31.5, and every one of them was run against the same small page: an email field, a Subscribe button, and a result element that appears 400ms after the click. Here is the same test twice.
// wdio/test/specs/signup.e2e.ts — WebdriverIO 9.31.5
import { browser, $, expect } from '@wdio/globals'
describe('newsletter signup', () => {
it('confirms the address it was given', async () => {
await browser.url('/')
await $('#email').setValue('ada@example.com')
await $('button[type="submit"]').click()
await expect($('#result')).toHaveText('Subscribed ada@example.com')
})
})
// pw/tests/signup.spec.ts — Playwright 1.62.1
import { test, expect } from '@playwright/test'
test('newsletter signup confirms the address it was given', async ({ page }) => {
await page.goto('/')
await page.getByLabel('Email').fill('ada@example.com')
await page.getByRole('button', { name: 'Subscribe' }).click()
await expect(page.locator('#result')).toHaveText('Subscribed ada@example.com')
})
Ten lines and eight, both green, and the thing you are choosing between is in neither of them. Both runners waited for the delayed result without being told to. The difference shows up one file up, in the config, and in what had to be installed to get there.
In an empty directory here, npm install @playwright/test@1.62 added 3 packages and
19MB. The four packages a WebdriverIO runner needs for the same job added 468 packages and
109MB: @wdio/cli, @wdio/local-runner,
@wdio/mocha-framework and @wdio/spec-reporter. Both figures are npm's
own count on this machine, and neither counts a browser. Playwright downloads its own builds
into a cache outside node_modules; WebdriverIO drove the Chrome that was already
installed and fetched a matching ChromeDriver itself.
A mocha adapter and a spec reporter are separate packages in WebdriverIO because they are separate choices. In Playwright the test framework and the reporters are the same package as the runner, so there is nothing to choose and nothing to install. With WebdriverIO you choose the pieces. With Playwright the pieces are chosen.
Does WebdriverIO use Selenium, and does the protocol still matter?
No. WebdriverIO speaks the W3C WebDriver protocol, which is a specification and not a product. Selenium speaks it, Appium speaks it, and speaking a standard that somebody else also speaks is not the same as being them. The Selenium case is argued separately and it does not transfer here.
WebdriverIO also speaks WebDriver BiDi, and its documentation is specific about when: "By default, WebdriverIO will attempt to start a local automation session using the WebDriver Bidi protocol." The same page calls BiDi "the successor of the WebDriver protocol", one that "enables a lot more introspection capabilities for various testing use cases", and adds that it "is currently under development and new primitives might be added in the future".
Our run was more mixed than either sentence suggests.
WebdriverIO 9.31.5 started ChromeDriver on a local port, opened the session over classic
WebDriver, then registered a BiDi handler and connected a WebSocket to that same session — after
which it subscribed to browsingContext, log and network
events. In the run logged for this page, one test produced 13 BiDi commands and 10 classic HTTP
calls. The two are not alternatives you pick between; both were in use in the same session.
Playwright launches its own browser build and talks to it over a pipe. On 1.62.1 the browser
process is started with --remote-debugging-pipe under Chromium,
-juggler-pipe under Firefox and --inspector-pipe under WebKit, and it
listens on no port at all — observed by reading the process, because Playwright documents
none of it. What sits between your test and
the browser goes through that in detail, and this page borrows it.
So the two projects differ in the shape of the link and not only in the protocol on it: one side is HTTP requests and a BiDi socket to a browser that is listening, the other is a pipe to a browser that is not. Neither shape is evidence of speed. A pipe removes a network stack from the path and adds nothing that makes a click faster, and no measurement below turns on it.
Neither project publishes a measurement of the two transports on the same workload, and this page does not supply one.
What does WebdriverIO's ecosystem give you that Playwright does not?
WebdriverIO publishes an extension surface with two named shapes in it. Its documentation
describes services as "add-ons that are created for reusable logic to simplify tests, manage
your test suite and integrate results", and notes that they "have access to all the
same hooks available in the wdio.conf.js". On reporters it is blunter:
"You can write your own custom reporter for the WDIO test runner that is tailored to your
needs. And it’s easy!"
Both of those pages end with instructions for publishing the thing on npm and getting it added to the WebdriverIO CLI. The extension points are designed to be published, which is what the heading means by ecosystem.
Here are the two config files from the runs above, cut to the part that answers the question.
// wdio/wdio.conf.ts — WebdriverIO 9.31.5
import type { Options } from '@wdio/types'
export const config: Options.Testrunner = {
runner: 'local',
specs: ['./test/specs/**/*.ts'],
maxInstances: 1,
capabilities: [{
browserName: 'chrome',
'goog:chromeOptions': { args: ['--headless=new'] }
}],
framework: 'mocha',
services: [
['static-server', {
port: 4600,
folders: [{ mount: '/', path: './public' }]
}]
],
reporters: ['spec'],
baseUrl: 'http://localhost:4600',
logLevel: 'error',
mochaOpts: { ui: 'bdd', timeout: 60000 }
}
// pw/playwright.config.ts — Playwright 1.62.1
import { defineConfig, devices } from '@playwright/test'
export default defineConfig({
testDir: './tests',
reporter: [['list'], ['html', { open: 'never' }]],
webServer: {
command: 'node ./serve.cjs',
url: 'http://localhost:4600',
reuseExistingServer: false
},
use: {
baseURL: 'http://localhost:4600',
trace: 'on-first-retry'
},
projects: [
{ name: 'chromium', use: { ...devices['Desktop Chrome'] } },
{ name: 'firefox', use: { ...devices['Desktop Firefox'] } },
{ name: 'webkit', use: { ...devices['Desktop Safari'] } }
]
})
The WebdriverIO file is a list of decisions: which framework, which service, which reporter,
each one a package that had to be installed and each one a name you have to know. The
Playwright file is a list of settings on things that are already there. webServer
is an option rather than a plugin, projects bought three engines for three lines,
and reporter names two that shipped with the runner. Which of those two files a
team wants to own is most of this decision.
The cost of the first shape showed up while we were writing it. The obvious npm result for a
static file server, wdio-static-server-service, is a package last published at
version 1.0.1. It installed cleanly, its hook ran, it served nothing, and the suite failed on
ERR_CONNECTION_REFUSED against a port with no server behind it. The one that works
is @wdio/static-server-service, versioned 9.31.2 in step with the runner. Both are
real packages with almost the same name, and the config file gives no sign which is which until
the run fails. A plugin list is capability somebody else wrote, and it is a dependency list
somebody on your team keeps current.
Can Playwright test a native mobile app?
No. Playwright drives browsers. It emulates a mobile browser — a viewport, a user agent, touch input — which covers responsive web at phone size, and none of that is the app, the store build or the device. Playwright does have an Android API, and it drives Chrome and WebView on an Android device over ADB, which is still a browser.
WebdriverIO goes where Playwright does not. Its Appium page opens: "With WebdriverIO you can test not only web application in the browser but also other platforms such as:" and lists mobile applications on iOS, Android or Tizen, desktop applications on macOS or Windows, and TV apps for Roku, tvOS, Android TV and Samsung. If your suite drives any of those out of the same config as your web tests, that is a reason to keep WebdriverIO that no amount of tooling on our side answers.
Is WebdriverIO still worth using?
Yes. The project's versions page lists v9 as the current stable line, it is governed under the OpenJS Foundation, and its documentation carries a 2026 copyright. It describes itself as a progressive automation framework for modern web and mobile applications, and on the evidence of a working install that is what it is.
WebdriverIO publishes its own argument against tools like ours. Under Based on Web Standards it says: "While other automation tools require you to download modified browser engines that aren't used by actual users or emulate user behavior by injecting JavaScript, WebdriverIO relies on a common agreed standard for automation that is properly tested and ensures compatibility for decades to come."
Half of that lands on us, and our own documentation says so. Playwright's browser page states that Playwright does not work with the branded version of Safari, because it relies on patches, and it says the same of Firefox. Its WebKit is a build from WebKit main, not the Safari your users have, and no configuration changes that. The runs above are the same story from the other end: WebdriverIO drove the Chrome 152 already installed on the machine, and Playwright downloaded and pinned its own Chromium, Firefox and WebKit builds. The other half of the sentence does not land, because Playwright drives the page over its own protocol and injects no behaviour into it, and it runs branded Chrome and Edge when you ask it to. The first half is accurate, and it is the same limit this company already states on its own service pages.
So there are situations where WebdriverIO is the better answer for a team that has it. A suite that touches a native app, a desktop app or a TV app, none of which Playwright can drive. A team that has already written services it depends on, where the plugin list is an asset with a history in it. A team whose objection is the one quoted above, that the browser under test should be the browser the user has. And, if your CI images are old, a wider floor: WebdriverIO 9's package manifest declares Node 18.20.0 and up, while Playwright's install page names the latest 22.x, 24.x or 26.x.
Should you move a WebdriverIO suite to Playwright?
Check these against your own repository.
- Move it if the estate is web only and the plugin list has become a maintenance job of its own — versions to keep in step, a package nobody remembers adding, a service whose upstream has gone quiet.
- Move it if the thing you keep wanting after a CI failure is a recording of what the browser did, rather than another log line.
- Move it if a second and third engine in CI should cost three lines of
config. The
projectsblock above is the whole of it. - Stay if any part of the suite drives a native app, a desktop app or a TV app. Half a suite does not move.
- Stay if the suite is green, the config is understood, and the only argument for moving is that Playwright is what people use now. That is not a reason.
If your estate is web only and the answer is yes, the next question is what happens to the specs, the services and the pipeline you already have — which is the job the migration page describes.
When this bites you
A team runs web and native mobile out of one WebdriverIO config. They read a comparison, move the web half to Playwright, and now own two runners, two config files, two reporter setups and two CI jobs. The WebdriverIO installation did not go away, because the mobile specs still need it. Nothing was removed and the maintenance surface went up, on the strength of an argument about the web half in isolation.
From inside, it looks like this: the second runner is nobody's job, its dependencies go stale first, and the first time it breaks it breaks on a release day. Moving the web half on purpose, with the mobile half treated as a separate suite that has its own owner, is a legitimate outcome. Drifting into it because a comparison article only talked about browsers is not.
The second bite is quieter. A WebdriverIO config carries services somebody added for a reason nobody wrote down: a reporter that feeds a dashboard, a service that seeds a database, a step that a release manager reads on Fridays. The conversion estimate is made of exactly that list, and it is the part of the suite with no equivalent anywhere in the specs.
Questions
No People Also Ask set exists for this pair in our data, so these are argued from search intent.
Is Playwright better than WebdriverIO?
At being one assembled thing, yes. Playwright ships the runner, the assertions, three browser engines and the trace viewer in one package, so there is less to choose and less to keep current. WebdriverIO is better at being extended, and it reaches platforms Playwright does not reach at all. Neither is better at being everything. If your estate is web only, Playwright is less to own; if any part of it drives a native app, the comparison stops being about browsers.
Can Playwright test native mobile apps?
No. Playwright drives browsers, and what it offers for mobile is emulation of a mobile browser: a viewport, a user agent and touch input. That covers responsive web at phone size. It is not the app, not the store build and not the device. Playwright's Android API drives Chrome and WebView on an Android device over ADB, which is a browser on a phone rather than your application. A suite driving a native iOS or Android build has nowhere to land in Playwright, and that is a fact about the framework rather than a preference of ours.
Is WebdriverIO still maintained?
Yes. The project's versions page lists v9 as the current stable line, and the project is governed under the OpenJS Foundation, whose copyright appears on the documentation for 2026. We installed WebdriverIO 9.31.5 from npm for this article and ran a suite on it.
Does WebdriverIO use Selenium?
No. WebdriverIO speaks the W3C WebDriver protocol and WebDriver BiDi. WebDriver is a standard rather than a product, and Selenium and Appium speak it too, so speaking it is not the same as being Selenium. In our run WebdriverIO 9.31.5 started ChromeDriver itself on a local port, opened the session over classic WebDriver, then connected a WebDriver BiDi WebSocket to that same session. No Grid and no Selenium package were involved.
Is Playwright faster than WebdriverIO?
We have run no benchmark, so there is no figure on this page and no multiplier. The two suites here are one test each against a page with a deliberate 400ms delay in it, which measures nothing you could carry to your own repository. A result worth quoting would take one real workload, run it under both, hold the application, the machine, the browser build and the network constant, say which of those it held, and report how often each run had to be repeated.
What is in your wdio.conf.ts?
The services and reporters arrays, roughly how many specs sit behind them, and whether any of those specs drive something that is not a browser. The boundary is the destination: a suite driving a native iOS or Android application has nowhere to land in Playwright.