---
title: "Lighthouse on Authenticated Pages with Playwright"
description: "Run Lighthouse audits on pages behind login. Learn how to preserve authentication state when Lighthouse opens a new browser context."
canonical_url: "https://unlighthouse.dev/learn-lighthouse/playwright/authentication"
last_updated: "2026-07-05T10:28:41.119Z"
---

Running Lighthouse on pages that require authentication is tricky. Lighthouse opens a fresh page context, which means your login state doesn't automatically carry over.

## The Problem

When you navigate to a protected page with [Playwright](https://playwright.dev) and then run Lighthouse:

```js
// This won't work as expected
await page.goto('https://app.example.com/login')
await page.fill('#email', 'user@example.com')
await page.fill('#password', 'password')
await page.click('button[type="submit"]')
await page.waitForURL('**/dashboard')

// Lighthouse opens a NEW page — login state is lost
const result = await lighthouse('https://app.example.com/dashboard', { port: PORT })
// ❌ Redirects to login page
```

Lighthouse creates its own page to run audits, discarding Playwright's authenticated session.

## Solution: Storage State

Playwright can save and restore session state (cookies, localStorage, sessionStorage). Save it after login, then configure Lighthouse to use the same browser context.

### Step 1: Save Authentication State

```js
import { writeFileSync } from 'node:fs'
import { chromium } from 'playwright'

const PORT = 9222

async function saveAuthState() {
  const browser = await chromium.launch({
    args: [`--remote-debugging-port=${PORT}`],
  })

  const context = await browser.newContext()
  const page = await context.newPage()

  // Perform login
  await page.goto('https://app.example.com/login')
  await page.fill('#email', 'user@example.com')
  await page.fill('#password', 'password')
  await page.click('button[type="submit"]')
  await page.waitForURL('**/dashboard')

  // Save storage state
  const storageState = await context.storageState()
  writeFileSync('auth.json', JSON.stringify(storageState))

  await browser.close()
}
```

### Step 2: Run Lighthouse with Saved State

```js
import { readFileSync } from 'node:fs'
import lighthouse from 'lighthouse'
import { chromium } from 'playwright'

const PORT = 9222

async function auditAuthenticatedPage(url) {
  const browser = await chromium.launch({
    args: [`--remote-debugging-port=${PORT}`],
  })

  // Load saved authentication state
  const storageState = JSON.parse(readFileSync('auth.json', 'utf-8'))
  const context = await browser.newContext({ storageState })
  const page = await context.newPage()

  // Navigate to authenticated page
  await page.goto(url, { waitUntil: 'networkidle' })

  // Run Lighthouse with storage reset disabled
  const result = await lighthouse(url, {
    port: PORT,
    disableStorageReset: true, // Critical: preserves cookies/storage
    logLevel: 'error',
  })

  await browser.close()
  return result.lhr
}

auditAuthenticatedPage('https://app.example.com/dashboard')
```

<warning>

**Critical**: Always set `disableStorageReset: true`. Without this, Lighthouse clears cookies and storage before the audit, logging you out.

</warning>

## Complete Working Example

Full script that handles login and audit in one flow:

```js
import { existsSync, readFileSync, writeFileSync } from 'node:fs'
import lighthouse from 'lighthouse'
import { chromium } from 'playwright'

const PORT = 9222
const AUTH_FILE = 'auth.json'

async function login(context) {
  const page = await context.newPage()
  await page.goto('https://app.example.com/login')
  await page.fill('#email', process.env.TEST_USER_EMAIL)
  await page.fill('#password', process.env.TEST_USER_PASSWORD)
  await page.click('button[type="submit"]')
  await page.waitForURL('**/dashboard')
  await page.close()
}

async function auditWithAuth(url) {
  const browser = await chromium.launch({
    args: [`--remote-debugging-port=${PORT}`],
  })

  let context

  // Reuse saved auth if available
  if (existsSync(AUTH_FILE)) {
    const storageState = JSON.parse(readFileSync(AUTH_FILE, 'utf-8'))
    context = await browser.newContext({ storageState })
  }
  else {
    context = await browser.newContext()
    await login(context)
    // Save for future runs
    const state = await context.storageState()
    writeFileSync(AUTH_FILE, JSON.stringify(state))
  }

  const page = await context.newPage()
  await page.goto(url, { waitUntil: 'networkidle' })

  const result = await lighthouse(url, {
    port: PORT,
    disableStorageReset: true,
    output: 'html',
  })

  writeFileSync('lighthouse-report.html', result.report)
  await browser.close()

  return result.lhr
}

auditWithAuth('https://app.example.com/dashboard')
```

## Simplifying with Playwright Project Dependencies

Modern Playwright (v1.31+) allows defining [setup projects](https://playwright.dev/docs/test-global-setup-teardown#project-dependencies). This separates login logic from your tests.

**1. Configure playwright.config.ts:**

```ts
import { defineConfig } from '@playwright/test'

export default defineConfig({
  projects: [
    { name: 'setup', testMatch: /.*\.setup\.ts/ },
    {
      name: 'lighthouse',
      use: {
        // Automatically load auth state
        storageState: 'playwright/.auth/user.json',
      },
      dependencies: ['setup'], // Run setup first
    },
  ],
})
```

**2. Create setup file auth.setup.ts:**

```ts
import { test as setup } from '@playwright/test'

setup('authenticate', async ({ page }) => {
  await page.goto('https://app.example.com/login')
  // ... perform login ...
  await page.context().storageState({ path: 'playwright/.auth/user.json' })
})
```

**3. Run audit (no login logic needed):**

```ts
test('dashboard performance', async ({ page }) => {
  // Page is already authenticated!
  await page.goto('https://app.example.com/dashboard')

  // Connect Lighthouse to this authenticated session
  // (Use the port and disableStorageReset pattern)
})
```

## Alternative: Cookie Injection via CDP

For token-based auth where `storageState` doesn't work (e.g., custom auth headers), inject cookies directly:

```js
import lighthouse from 'lighthouse'
import { chromium } from 'playwright'

const PORT = 9222

async function auditWithCookies(url, cookies) {
  const browser = await chromium.launch({
    args: [`--remote-debugging-port=${PORT}`],
  })

  const context = await browser.newContext()

  // Add cookies to context
  await context.addCookies(cookies)

  const page = await context.newPage()
  await page.goto(url, { waitUntil: 'networkidle' })

  const result = await lighthouse(url, {
    port: PORT,
    disableStorageReset: true,
  })

  await browser.close()
  return result.lhr
}

// Usage
auditWithCookies('https://app.example.com/dashboard', [
  {
    name: 'session_token',
    value: 'your-token-here',
    domain: 'app.example.com',
    path: '/',
  },
])
```

## Handling Session Expiry

Long test runs cause session tokens to expire. Add a check:

```js
async function verifyAuthenticated(page, context) {
  await page.goto('https://app.example.com/dashboard')

  // Check if redirected to login
  if (page.url().includes('/login')) {
    await login(context)
    const state = await context.storageState()
    writeFileSync(AUTH_FILE, JSON.stringify(state))
    await page.goto('https://app.example.com/dashboard')
  }
}
```

## Common Pitfalls

<table>
<thead>
  <tr>
    <th>
      Issue
    </th>
    
    <th>
      Solution
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      Session lost after audit
    </td>
    
    <td>
      Add <code>
        disableStorageReset: true
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      CSRF token invalid
    </td>
    
    <td>
      Re-login before each audit
    </td>
  </tr>
  
  <tr>
    <td>
      Cookies not applying
    </td>
    
    <td>
      Check <code>
        domain
      </code>
      
       matches exactly
    </td>
  </tr>
  
  <tr>
    <td>
      Auth works in Playwright but not Lighthouse
    </td>
    
    <td>
      Lighthouse uses a new page - check that cookies are on the context, not just the page
    </td>
  </tr>
</tbody>
</table>
