Lighthouse GitHub Actions: Automated SEO and Performance Monitoring
Lighthouse audits web pages for performance, accessibility, SEO, and best practices. Running Lighthouse manually before every deploy doesn't scale. GitHub Actions automates Lighthouse audits on every pull request, catching regressions before they reach production.This guide shows how to integrate Lighthouse with GitHub Actions, configure SEO-focused audits, and set up alerting for performance and SEO issues.
Why Automate Lighthouse Audits
Manual Lighthouse audits face three problems:
Inconsistency: Developers forget to run audits, or only audit on desktop, or skip audits when rushing to deploy. Late detection: Discovering a performance regression after deployment means rollback complexity, angry users, and lost rankings. No historical tracking: Manual audits produce one-time scores with no trend data to identify gradual degradation. Automated Lighthouse via GitHub Actions solves these: Consistency: Every pull request runs audits automatically, no human memory required. Early detection: Regressions get caught during code review, before merging to main. Failed audits block merges. Historical tracking: Store audit results in artifacts or external services to track performance trends over time. SEO-specific use cases:- Catch meta tag removals or canonical tag errors before deployment
- Detect structured data breakage or schema.org validation errors
- Monitor crawlability issues (broken internal links, robots.txt changes)
- Track Core Web Vitals trends (LCP, CLS, FID) across releases
Setting Up Lighthouse GitHub Actions
Basic Workflow Configuration
Create a GitHub Actions workflow file at .github/workflows/lighthouse.yml:
What this does:name: Lighthouse CIon: pull_request: branches: - main
jobs: lighthouse: runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkout@v3
- name: Install Node.js uses: actions/setup-node@v3 with: node-version: '18'
- name: Install dependencies run: npm install
- name: Build site run: npm run build
- name: Run Lighthouse uses: treosh/lighthouse-ci-action@v9 with: urls: | https://example.com https://example.com/blog https://example.com/products uploadArtifacts: true temporaryPublicStorage: true
- Triggers on pull requests to
main - Checks out code
- Installs dependencies and builds the site
- Runs Lighthouse against specified URLs
- Uploads audit results as artifacts
Lighthouse CI Server Setup (Advanced)
For persistent storage and historical trends, use Lighthouse CI Server.
Benefits:- Stores audit history across all branches and PRs
- Provides web UI for comparing runs
- Enables trend analysis (performance over time)
- Supports assertions (fail builds if scores drop below thresholds)
- Deploy Lighthouse CI Server (Heroku, Vercel, or self-hosted Docker container)
- Update workflow to post results to the server:
- name: Run Lighthouse CI
uses: treosh/lighthouse-ci-action@v9
with:
urls: |
https://example.com
https://example.com/blog
serverBaseUrl: https://your-lhci-server.herokuapp.com
serverToken: ${{ secrets.LHCI_SERVER_TOKEN }}
- Add assertions in a
lighthouserc.jsonconfig:
{
"ci": {
"collect": {
"numberOfRuns": 3
},
"assert": {
"assertions": {
"categories:performance": ["error", {"minScore": 0.9}],
"categories:seo": ["error", {"minScore": 0.95}],
"categories:accessibility": ["warn", {"minScore": 0.9}]
}
},
"upload": {
"target": "lhci",
"serverBaseUrl": "https://your-lhci-server.herokuapp.com",
"token": "YOUR_TOKEN"
}
}
}
Assertions:
categories:performancerequires performance score ≥90categories:seorequires SEO score ≥95categories:accessibilitywarns if accessibility <90 but doesn't fail
SEO-Focused Lighthouse Configuration
Lighthouse's SEO audit checks 16+ factors. Customize which failures block deployments.
SEO Audit Categories
Lighthouse SEO audits include:
Document structure:</code> tag exists and isn't empty</li> <li>Meta description exists</li> <li>Page has valid <code><html lang></code> attribute</li></ul> <strong>Crawlability:</strong> <ul><li><code>robots.txt</code> is valid</li> <li>Page is mobile-friendly</li> <li>Links have descriptive text (no "click here")</li> <li>Links are crawlable (use <code><a href></code>, not JavaScript click handlers)</li></ul> <strong>Structured data:</strong> <ul><li>Structured data is valid (schema.org compliance)</li></ul> <strong>Image optimization:</strong> <ul><li>Images have <code>alt</code> attributes</li> <li>Images use appropriate sizes</li></ul> <strong>Mobile usability:</strong> <ul><li>Viewport meta tag exists</li> <li>Font sizes are legible on mobile</li> <li>Tap targets are appropriately sized (44x44px minimum)</li></ul> <h3>Custom SEO Assertions</h3> <p>Target critical SEO issues in <code>lighthouserc.json</code>:</p> <pre><code class="language-json">{ "ci": { "assert": { "assertions": { "categories:seo": ["error", {"minScore": 0.95}], "document-title": "error", "meta-description": "error", "link-text": "warn", "crawlable-anchors": "error", "image-alt": "warn", "hreflang": "error", "canonical": "error" } } } }</code></pre> <strong>Critical errors (block merge):</strong> <ul><li>Missing <code><title></code> or meta description</li> <li>Invalid canonical tags or hreflang</li> <li>Links that aren't crawlable</li></ul> <strong>Warnings (don't block, but flag):</strong> <ul><li>Generic link text ("click here", "read more")</li> <li>Missing alt attributes</li></ul> <h3>Validating Structured Data</h3> <p>Lighthouse validates structured data but doesn't exhaustively check schema.org compliance. Add a separate step for schema validation:</p> <pre><code class="language-yaml">- name: Validate Structured Data run: | npx structured-data-testing-tool https://example.com</code></pre> <p>Alternatively, integrate <strong>Google's Rich Results Test</strong> via API or scraping.</p> <h2>Core Web Vitals Tracking</h2> <p>Lighthouse measures <strong>Core Web Vitals</strong> (LCP, CLS, FID/TBT). Track these over time to identify regressions.</p> <h3>Web Vitals Assertions</h3> <p>Set thresholds in <code>lighthouserc.json</code>:</p> <pre><code class="language-json">{ "ci": { "assert": { "assertions": { "largest-contentful-paint": ["error", {"maxNumericValue": 2500}], "cumulative-layout-shift": ["error", {"maxNumericValue": 0.1}], "total-blocking-time": ["error", {"maxNumericValue": 300}] } } } }</code></pre> <strong>Thresholds:</strong> <ul><li><strong>LCP:</strong> ≤2.5s (good), 2.5-4.0s (needs improvement), >4.0s (poor)</li> <li><strong>CLS:</strong> ≤0.1 (good), 0.1-0.25 (needs improvement), >0.25 (poor)</li> <li><strong>TBT (proxy for FID):</strong> ≤300ms (good), 300-600ms (needs improvement), >600ms (poor)</li></ul> Pull requests that exceed thresholds fail, forcing developers to optimize before merging. <h3>Tracking Trends</h3> <p>Store Lighthouse scores in a database or external service to track trends.</p> <strong>Option 1: Lighthouse CI Server</strong> (built-in trending) <strong>Option 2: Post results to analytics platform</strong> <pre><code class="language-yaml">- name: Post Lighthouse results to analytics run: | curl -X POST https://analytics.example.com/lighthouse \ -H "Content-Type: application/json" \ -d '{"score": ${{ steps.lighthouse.outputs.performance }}, "timestamp": "$(date -Iseconds)"}'</code></pre> <strong>Option 3: Store results in Google Sheets or Airtable</strong> <p>Use a GitHub Action like <code>googleapis/sheets-action</code> to append scores to a tracking sheet.</p> <h2>Handling Dynamic and Authenticated Pages</h2> <p>Lighthouse audits require publicly accessible URLs. Staging environments or authenticated pages need special handling.</p> <h3>Auditing Staging Environments</h3> <p>Deploy PR branches to preview environments (Vercel, Netlify, or custom staging), then audit those URLs.</p> <strong>Example with Vercel:</strong> <pre><code class="language-yaml">- name: Deploy to Vercel uses: amondnet/vercel-action@v20 with: vercel-token: ${{ secrets.VERCEL_TOKEN }} vercel-org-id: ${{ secrets.VERCEL_ORG_ID }} vercel-project-id: ${{ secrets.VERCEL_PROJECT_ID }} <ul><li>name: Run Lighthouse on preview</li></ul> uses: treosh/lighthouse-ci-action@v9 with: urls: ${{ steps.vercel.outputs.preview-url }}</code></pre> <strong>This audits the deployed preview, catching issues before production.</strong> <h3>Auditing Authenticated Pages</h3> <p>Lighthouse can't audit pages behind login by default. Two workarounds:</p> <strong>Option 1: Bypass authentication for CI</strong> <p>Create a temporary token or bypass parameter that allows Lighthouse to access authenticated pages during CI runs. Restrict this to CI IP addresses.</p> <strong>Option 2: Puppeteer login flow</strong> <p>Use Puppeteer to log in before running Lighthouse:</p> <pre><code class="language-javascript">const puppeteer = require('puppeteer'); const lighthouse = require('lighthouse'); <p>(async () => { const browser = await puppeteer.launch(); const page = await browser.newPage();</p> <p>// Login await page.goto('https://example.com/login'); await page.type('#username', 'test-user'); await page.type('#password', 'test-password'); await page.click('#login-button'); await page.waitForNavigation();</p> <p>// Run Lighthouse const result = await lighthouse('https://example.com/dashboard', { port: new URL(browser.wsEndpoint()).port, output: 'json' });</p> <p>console.log(result.lhr.categories.seo.score);</p> <p>await browser.close(); })();</code></pre></p> <h2>Alerting on Regressions</h2> <p>GitHub Actions can send alerts when audits fail.</p> <h3>Slack Notifications</h3> <p>Post Lighthouse failures to Slack:</p> <pre><code class="language-yaml">- name: Notify Slack on failure if: failure() uses: slackapi/slack-github-action@v1 with: webhook-url: ${{ secrets.SLACK_WEBHOOK }} payload: | { "text": "Lighthouse audit failed on PR #${{ github.event.pull_request.number }}", "blocks": [ { "type": "section", "text": { "type": "mrkdwn", "text": "Performance score dropped below threshold. Review: ${{ github.event.pull_request.html_url }}" } } ] }</code></pre> <h3>Email Notifications</h3> <p>Use GitHub Actions' built-in email notifications or a custom action to send email alerts on failures.</p> <h3>PR Comments</h3> <p>Post Lighthouse results directly to pull requests:</p> <pre><code class="language-yaml">- name: Comment Lighthouse results on PR uses: actions/github-script@v6 with: script: | github.rest.issues.createComment({ issue_number: context.issue.number, owner: context.repo.owner, repo: context.repo.repo, body: 'Lighthouse Audit Results:\n- Performance: 92\n- SEO: 98\n- Accessibility: 95' });</code></pre> <h2>Example: Full Lighthouse GitHub Actions Workflow</h2> <pre><code class="language-yaml">name: Lighthouse CI <p>on: pull_request: branches: - main</p> <p>jobs: lighthouse: runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkout@v3</p> <p>- name: Setup Node.js uses: actions/setup-node@v3 with: node-version: '18'</p> <p>- name: Install dependencies run: npm install</p> <p>- name: Build site run: npm run build</p> <p>- name: Run Lighthouse CI uses: treosh/lighthouse-ci-action@v9 with: urls: | http://localhost:3000 http://localhost:3000/blog http://localhost:3000/products uploadArtifacts: true configPath: './lighthouserc.json'</p> <p>- name: Comment results on PR uses: actions/github-script@v6 with: script: | const fs = require('fs'); const results = JSON.parse(fs.readFileSync('.lighthouseci/manifest.json')); github.rest.issues.createComment({ issue_number: context.issue.number, owner: context.repo.owner, repo: context.repo.repo, body: <code>Lighthouse Results:\n- SEO: ${results[0].summary.seo <em> 100}\n- Performance: ${results[0].summary.performance </em> 100}</code> });</p> <p>- name: Notify Slack on failure if: failure() uses: slackapi/slack-github-action@v1 with: webhook-url: ${{ secrets.SLACK_WEBHOOK }} payload: | { "text": "Lighthouse audit failed for PR #${{ github.event.pull_request.number }}" }</code></pre></p> <h2>FAQ</h2> <strong>Does Lighthouse GitHub Actions work for server-side rendered apps?</strong> <p>Yes, but you must build and serve the app before running Lighthouse. Use <code>npm run build && npm run start</code> to serve a local instance, then audit <code>http://localhost:3000</code>.</p> <strong>Can I audit mobile and desktop separately?</strong> <p>Yes. Configure separate Lighthouse runs with different emulation settings in <code>lighthouserc.json</code>:</p> <pre><code class="language-json">{ "ci": { "collect": { "settings": { "emulatedFormFactor": "mobile" } } } }</code></pre> <p>Run the workflow twice with different configs, or use matrix builds to test both.</p> <strong>How do I prevent flaky Lighthouse scores?</strong> <p>Run Lighthouse multiple times per URL and average scores. Set <code>numberOfRuns: 3</code> in <code>lighthouserc.json</code> to run 3 audits per URL and use the median score.</p> <strong>What if Lighthouse fails due to third-party script issues?</strong> <p>Lighthouse audits the entire page, including third-party scripts. If a third-party script causes failures, either fix the issue with the vendor, remove the script, or adjust assertions to warn instead of error.</p> <strong>Can I run Lighthouse on every commit, not just PRs?</strong> <p>Yes, but this consumes more CI minutes. Use triggers like <code>push: branches: [main]</code> to audit on every commit to main. Most teams audit PRs only to save resources.</p> <strong>How do I benchmark against competitors?</strong> <p>Add competitor URLs to the <code>urls</code> list. Lighthouse audits any publicly accessible URL. Track your scores vs. competitors to identify gaps.</p> </div> </div> </main> <footer class="site-footer"> <div class="container"> <div class="footer-grid"> <div class="footer-col"> <h4>About SEO by Role</h4> <p>SEO education tailored to your job function. Executives, PMs, developers, content teams, founders, and marketing managers each get strategies designed for their constraints, responsibilities, and success metrics.</p> <p style="margin-top: 1rem;">A <a href="https://scalewithsearch.com" rel="me" style="color: #84cc16;">Scale With Search</a> property.</p> </div> <div class="footer-col"> <h4>By Role</h4> <ul> <li><a href='/executives'>Executives</a></li> <li><a href='/product-managers'>Product Managers</a></li> <li><a href='/developers'>Developers</a></li> <li><a href='/content-teams'>Content Teams</a></li> <li><a href='/founders'>Founders</a></li> <li><a href='/marketing-managers'>Marketing Managers</a></li> </ul> </div> <div class="footer-col"> <h4>Popular Guides</h4> <ul> <li><a href='/articles/seo-for-product-managers'>SEO for Product Managers</a></li> <li><a href='/articles/technical-seo-for-developers'>Technical SEO for Developers</a></li> <li><a href='/articles/seo-for-founders-seo-vs-paid'>SEO vs Paid for Founders</a></li> <li><a href='/articles/seo-responsibility-matrix'>SEO Responsibility Matrix</a></li> <li><a href='/articles/how-to-audit-seo-agency'>How to Audit an SEO Agency</a></li> <li><a href='/articles/seo-for-cmos-managing-seo-spend'>SEO Spend for CMOs</a></li> <li><a href='/articles/seo-for-content-teams-keyword-research'>Keyword Research for Content Teams</a></li> <li><a href='/articles/seo-forecasting-executive-scrutiny'>SEO Forecasting</a></li> </ul> </div> <div class="footer-col"> <h4>From Scale With Search</h4> <ul> <li><a href="https://scalewithsearch.com" rel="me">Scale With Search</a></li> <li><a href="https://aifirstsearch.com" rel="me">AI First Search</a></li> <li><a href="https://browserprompt.com" rel="me">Browser Prompt</a></li> <li><a href="https://seobyrole.com" rel="me">SEO by Role</a></li> </ul> </div> </div> <div class="footer-bottom"> <span>© 2026 SEO by Role. All rights reserved.</span> <div class="entity-links"> <a href="/sitemap.xml">Sitemap</a> <a href='/articles'>Articles</a> </div> </div> </div> </footer> <script> (function() { // Role dropdown toggle var dd = document.getElementById('roleDropdown'); if (dd) { var trigger = dd.querySelector('.nav-dropdown-trigger'); trigger.addEventListener('click', function(e) { e.stopPropagation(); dd.classList.toggle('open'); trigger.setAttribute('aria-expanded', dd.classList.contains('open')); }); document.addEventListener('click', function(e) { if (!dd.contains(e.target)) { dd.classList.remove('open'); trigger.setAttribute('aria-expanded', 'false'); } }); } // Mobile toggle var toggle = document.getElementById('navToggle'); var links = document.getElementById('navLinks'); if (toggle && links) { toggle.addEventListener('click', function() { links.classList.toggle('open'); }); } // FAQ accordion document.querySelectorAll('.faq-question').forEach(function(btn) { btn.addEventListener('click', function() { var item = btn.closest('.faq-item'); var wasOpen = item.classList.contains('open'); document.querySelectorAll('.faq-item').forEach(function(el) { el.classList.remove('open'); }); if (!wasOpen) item.classList.add('open'); }); }); })(); </script> </body> </html>