How to Achieve a Perfect 100 Lighthouse Score (And Keep It)
A perfect Lighthouse score is achievable with the right architecture. Learn the specific techniques that move each metric from yellow to green — and how to prevent regressions.
Understanding What Lighthouse Actually Measures
Lighthouse scores four categories: Performance, Accessibility, Best Practices, and SEO. Each category is scored 0-100. A "perfect 100" usually means hitting 100 in all four. Performance is the hardest because it's a composite of real-time metrics (FCP, LCP, TBT, CLS, SI).
Important caveat: Lighthouse is a lab tool. Real user performance (RUM) is what matters for business outcomes. But a high score means you've done the right things.
Performance: The Core Web Vitals Strategy
// Measure your starting point with programmatic Lighthouse
import lighthouse from 'lighthouse';
import * as chromeLauncher from 'chrome-launcher';
async function runLighthouse(url) {
const chrome = await chromeLauncher.launch({ chromeFlags: ['--headless'] });
const options = { logLevel: 'info', output: 'json', port: chrome.port };
const runnerResult = await lighthouse(url, options);
await chrome.kill();
const { categories, audits } = runnerResult.lhr;
console.log('Performance:', categories.performance.score * 100);
console.log('LCP:', audits['largest-contentful-paint'].displayValue);
console.log('TBT:', audits['total-blocking-time'].displayValue);
console.log('CLS:', audits['cumulative-layout-shift'].displayValue);
return runnerResult.lhr;
}
LCP: Get It Under 2.5s
- Preload your hero image:
<link rel="preload" as="image" href="/hero.webp"> - Use
fetchpriority="high"on the LCP image element - Avoid lazy-loading the LCP image
- Self-host fonts or use
font-display: swap - Remove render-blocking CSS from the critical path
<!-- Correct LCP image setup -->
<link rel="preload" as="image" href="/hero.webp" fetchpriority="high">
<img
src="/hero.webp"
alt="Hero"
fetchpriority="high"
loading="eager"
width="1200"
height="600"
/>
CLS: Eliminate Layout Shift
/* Always reserve space for images */
img {
width: 100%;
height: auto;
aspect-ratio: attr(width) / attr(height);
}
/* Reserve space for dynamic content */
.ad-slot {
min-height: 250px;
width: 300px;
}
/* Avoid inserting content above existing content */
.notification-banner {
position: fixed; /* doesn't shift document flow */
top: 0;
}
TBT: Break Up JavaScript
Total Blocking Time measures main-thread blocking. Each task over 50ms contributes. Target: under 200ms total TBT.
- Code-split large bundles
- Defer non-critical third-party scripts
- Move expensive computation to Web Workers
- Use
scheduler.yield()to break up long tasks
Accessibility: Common Missed Items
- All
<img>need meaningfulaltattributes - Buttons need accessible labels (not just icons)
- Form inputs need associated
<label>elements - Color contrast: 4.5:1 for normal text, 3:1 for large text
- Focus must be visible (no
outline: nonewithout alternative)
// Accessible icon button
function IconButton({ icon, label, onClick }) {
return (
<button
onClick={onClick}
aria-label={label}
className="p-2 rounded hover:bg-gray-100 focus:outline-none focus:ring-2 focus:ring-blue-500"
>
{icon}
</button>
);
}
SEO: The Easiest 100
- Every page needs a unique
<title>and<meta name="description"> - Proper heading hierarchy (one H1, logical H2-H6)
- All links need descriptive text (not "click here")
- Robots.txt and sitemap.xml configured
- Canonical tags on duplicate content
Preventing Regressions with CI
# .github/workflows/lighthouse.yml
- uses: treosh/lighthouse-ci-action@v11
with:
urls: |
https://your-preview-url.vercel.app
budgetPath: ./lighthouse-budget.json
uploadArtifacts: true
temporaryPublicStorage: true
A perfect 100 is a starting point, not a destination. Set up CI to catch regressions before they reach production.
Admin
Cal.com
Open source scheduling — tự host booking system, thay thế Calendly. Free & privacy-first.
Vercel
Deploy Next.js app trong 30 giây. Free tier rộng rãi cho side projects.
Bình luận (0)
Đăng nhập để bình luận
Chưa có bình luận nào. Hãy là người đầu tiên!