I've now set up Playwright on two large sites, roughly a year apart. The second one is dramatically better to live with, and the reasons have almost nothing to do with Playwright itself.
Fixtures over page objects
The first project used classic page objects: a class per page, methods for each action. It worked, and it slowly became a second application to maintain — with its own inheritance hierarchy and its own bugs.
The second uses Playwright's fixtures. A fixture is just a function that sets something up and tears it down, and tests declare what they need in their arguments. No inheritance, no shared mutable state, and setup cost is paid only by the tests that actually need it.
export const test = base.extend<{ signedIn: Page }>({
signedIn: async ({ page }, use) => {
await page.goto("/login");
// ... authenticate
await use(page);
},
});Roles, not CSS selectors
Every selector that reaches into the DOM structure is a test that will break during a refactor without finding a real bug. getByRole("button", { name: "Save" }) breaks only when the button genuinely stops being a button called Save.
The side benefit is that it's an accessibility test you didn't have to write. If you can't select an element by its role and accessible name, a screen reader user can't find it either.
Kill every arbitrary wait
waitForTimeout is the single biggest source of flakiness, and it's flaky in the worst direction: it passes locally and fails on a loaded CI runner. Every instance is either a missing assertion or a real race condition in the app.
Web-first assertions retry for you. await expect(locator).toBeVisible() already waits. If you find yourself needing a sleep, the honest fix is usually to assert on the thing you were actually waiting for.
Sharding, and being ruthless about the pyramid
A suite that takes twenty minutes gets skipped. Sharding across CI runners is a config change and bought us most of the way back.
But the bigger lever was deleting tests. A lot of what was in the E2E suite was really unit-level logic wearing a browser as a costume. Moving those down made the remaining E2E tests both faster and more meaningful — they now describe user journeys, which is the only thing E2E is uniquely good at.
Trace on first retry
trace: "on-first-retry" costs nothing on green runs and turns a failure into a DOM snapshot, a network log and a timeline. It is the difference between debugging a CI failure in five minutes and re-running the job hoping it goes away.