Lighthouse on Authenticated Pages with Playwright
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 and then run Lighthouse:
// 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
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
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')
disableStorageReset: true. Without this, Lighthouse clears cookies and storage before the audit, logging you out.Complete Working Example
Full script that handles login and audit in one flow:
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')
Streamlining with Playwright Project Dependencies
Modern Playwright (v1.31+) allows defining setup projects. This separates login logic from your tests.
1. Configure playwright.config.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:
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):
test('dashboard performance', async ({ page }) => {
// Page is already authenticated!
await page.goto('https://app.example.com/dashboard')
// Connect Lighthouse to this authenticated session
// (Ensure you 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:
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 can cause session tokens to expire. Add a check:
async function ensureAuthenticated(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
| Issue | Solution |
|---|---|
| Session lost after audit | Add disableStorageReset: true |
| CSRF token invalid | Re-login before each audit |
| Cookies not applying | Check domain matches exactly |
| Auth works in Playwright but not Lighthouse | Lighthouse uses a new page — ensure cookies are on the context, not just the page |
Related
- Troubleshooting — More auth debugging tips
- CI/CD Integration — Handle auth in pipelines
- Playwright Guide — Basic integration setup