Why are Python Playwright async tests so slow when using `async_playwright()` and how can I share the `page` object to speed them up?

I’m migrating TypeScript Playwright tests to Python and noticed that three simple async tests that navigate to playwright.dev take ~50 seconds when using:

from playwright.async_api import async_playwright

def test_has_title():
    with async_playwright() as p:
        browser = p.chromium.launch()
        context = browser.new_context()
        page = context.new_page()

        page.goto("https://playwright.dev/")
        print(page.title())

Even using sync_playwright with a page object reduces it to ~30 seconds. I tried using an async Page object with pytest-asyncio like this:

import pytest
from playwright.async_api import Page

@pytest.mark.asyncio
async def test_has_title(page: Page):
    await page.goto("https://playwright.dev/")
    print(await page.title())

but I can’t seem to share the page object efficiently among tests. How can I reuse the page object asynchronously to speed up test execution?

The main reason your async tests are slow is that each test creates a new browser and context, which is expensive. You can speed things up by sharing a single browser/page across tests using a pytest-asyncio fixture:

import pytest
from playwright.async_api import async_playwright, Page, Browser, BrowserContext

@pytest.fixture(scope="session")
async def browser_context():
    async with async_playwright() as p:
        browser = await p.chromium.launch()
        context = await browser.new_context()
        yield context
        await browser.close()

@pytest.fixture
async def page(browser_context: BrowserContext) -> Page:
    page = await browser_context.new_page()
    yield page
    await page.close()

@pytest.mark.asyncio
async def test_has_title(page: Page):
    await page.goto("https://playwright.dev/")
    print(await page.title())

@pytest.mark.asyncio
async def test_another(page: Page):
    await page.goto("https://playwright.dev/docs/intro")
    print(await page.title())

This way, the browser and context are created once per session, and each test gets a fresh page. Tests become much faster.

If your tests don’t need a completely isolated page per test, you can reuse the same page instance:

@pytest.fixture(scope="session")
async def page():
    async with async_playwright() as p:
        browser = await p.chromium.launch()
        context = await browser.new_context()
        page = await context.new_page()
        yield page
        await browser.close()

@pytest.mark.asyncio
async def test_one(page):
    await page.goto("https://playwright.dev/")
    print(await page.title())

@pytest.mark.asyncio
async def test_two(page):
    await page.goto("https://playwright.dev/docs/intro")
    print(await page.title())

This eliminates the overhead of opening a new page or context for each test, making tests much faster.

Another approach is to use an autouse fixture to automatically provide a shared page for all async tests:

@pytest.fixture(scope="session", autouse=True)
async def shared_page(request):
    async with async_playwright() as p:
        browser = await p.chromium.launch()
        context = await browser.new_context()
        page = await context.new_page()
        request.cls.page = page
        yield
        await browser.close()

@pytest.mark.asyncio
class TestPlaywright:
    async def test_home(self):
        await self.page.goto("https://playwright.dev/")
        print(await self.page.title())

    async def test_docs(self):
        await self.page.goto("https://playwright.dev/docs/intro")
        print(await self.page.title())

This pattern is handy when you have many tests in a class and want to reuse the same page instance without passing it explicitly to every test.