Achieving Lighthouse 95+ scores is not about chasing a number — it's about engineering a performant experience that directly impacts SaaS conversion rates. This playbook covers Core Web Vitals optimization, SSR with streaming, Critical CSS, bundle splitting, image optimization, and caching strategies. Every recommendation is actionable and production-tested. A 30-point checklist is included at the end.
Introduction: Why Lighthouse 95+ Matters for SaaS
In SaaS, every millisecond counts. Google's research has repeatedly shown that a 1-second delay in mobile page load reduces conversion rates by up to 20%. For a B2B SaaS product with a $100 monthly ARPU and 10,000 signups per month, that delay translates to over $2 million in annual revenue loss.
But the impact goes deeper:
- SEO Rankings: Google uses Core Web Vitals as a ranking signal. A site scoring below 50 on Lighthouse is unlikely to rank competitively.
- User Trust: Lighthouse scores correlate strongly with perceived reliability.
- Ad Spend Efficiency: Lower Quality Scores on Google Ads mean higher cost-per-click.
Lighthouse 95+ is a compound growth lever that touches acquisition, activation, and retention simultaneously.
Understanding Core Web Vitals
| Metric | What It Measures | Target |
|---|---|---|
| LCP (Largest Contentful Paint) | Loading performance | <= 2.5s |
| CLS (Cumulative Layout Shift) | Visual stability | <= 0.1 |
| INP (Interaction to Next Paint) | Interactivity | <= 200ms |
Performance Budgets: Set Targets from Day One
Define budgets for:
- JavaScript bundle size: 150 KB (compressed) for critical route, 300 KB max
- Time to Interactive: < 3s on 3G
- LCP: < 2.0s
- Total page weight: < 500 KB (compressed)
Enforce in CI:
// lighthouse-budget.js
module.exports = {
ci: {
collect: { numberOfRuns: 3, settings: { preset: 'desktop' } },
assert: {
assertions: {
'categories:performance': ['error', { minScore: 0.95 }],
'largest-contentful-paint': ['error', { maxNumericValue: 2500 }],
'cumulative-layout-shift': ['error', { maxNumericValue: 0.1 }],
'total-blocking-time': ['error', { maxNumericValue: 200 }],
},
},
},
};
LCP Optimization
1. SSR with Streaming
TanStack Start supports streaming SSR out of the box. Ensure you are not disabling it:
const router = createRouter({
routeTree,
defaultPreload: 'intent',
// Streaming is enabled by default in TanStack Start
});
2. Critical CSS
Inline the CSS required for above-the-fold content. Defer the rest:
<head>
<style>
/* Critical CSS — inline above-the-fold styles */
header, .hero, .cta-button { /* essential styles */ }
</style>
<link rel="preload" href="/styles/full.css" as="style"
onload="this.onload=null;this.rel='stylesheet'" />
<noscript><link rel="stylesheet" href="/styles/full.css" /></noscript>
</head>
3. Font Loading
Use font-display: swap and preload your primary font:
@font-face {
font-family: 'Inter';
src: url('/fonts/inter-var.woff2') format('woff2');
font-display: swap;
font-weight: 100 900;
}
Preload it in the HTML head:
<link rel="preload" href="/fonts/inter-var.woff2" as="font" type="font/woff2" crossorigin />
4. Optimize the LCP Element
-
Preload it:
<link rel="preload" as="image" href="https://webproxy.poorya-velaei-d67.workers.dev/https://dev.to/hero.webp" /> - Use responsive images for different viewports
- Avoid lazy loading the LCP element
CLS Optimization
1. Set Explicit Dimensions
<img
src="/dashboard-screenshot.webp"
width="1200"
height="675"
alt="Dashboard preview"
loading="lazy"
/>
2. Font Fallback Metrics
@font-face {
font-family: 'Inter Fallback';
src: local('Arial');
size-adjust: 107%;
ascent-override: 90%;
descent-override: 22%;
line-gap-override: 0%;
}
3. Reserve Space for Dynamic Content
.newsletter-signup-placeholder {
min-height: 120px;
/* Reserve space while async content loads */
}
JavaScript Optimization
1. Route-Based Code Splitting
TanStack Router supports route-based code splitting natively:
// routes/dashboard.tsx
export const Route = createLazyRoute('/dashboard')({
component: DashboardComponent,
});
2. Tree Shaking
- Use named imports:
import { useQuery } from '@tanstack/react-query' - Mark side-effect-free packages:
"sideEffects": false
3. Lazy Load Non-Critical Code
<script>
window.addEventListener('load', () => {
setTimeout(() => {
const script = document.createElement('script');
script.src = 'https://widget.example.com/loader.js';
script.async = true;
document.body.appendChild(script);
}, 3000);
});
</script>
Image Optimization
1. Responsive Images with srcset
<img
src="/hero-1200.webp"
srcset="/hero-400.webp 400w, /hero-800.webp 800w, /hero-1200.webp 1200w"
sizes="(max-width: 600px) 400px, (max-width: 1200px) 800px, 1200px"
width="1200" height="675" alt="Product dashboard"
/>
2. Modern Formats: WebP and AVIF
<picture>
<source srcset="/hero.avif" type="image/avif" />
<source srcset="/hero.webp" type="image/webp" />
<img src="/hero.jpg" width="1200" height="675" alt="Hero" />
</picture>
Caching Strategy
1. CDN Caching
Cache-Control: public, max-age=31536000, immutable # Versioned assets
Cache-Control: public, max-age=3600, s-maxage=86400 # HTML pages
2. TanStack Query Caching
const { data } = useQuery({
queryKey: ['pricing-plans'],
queryFn: fetchPricingPlans,
staleTime: 5 * 60 * 1000,
gcTime: 30 * 60 * 1000,
});
30-Point Performance Checklist
Critical
- [ ] LCP element is identified and preloaded
- [ ] All images have explicit width and height
- [ ] Critical CSS is inlined, full CSS is deferred
- [ ] Custom fonts use
font-display: swap - [ ] No render-blocking third-party scripts
- [ ] LCP image uses WebP/AVIF with responsive srcset
- [ ] Server response time (TTFB) < 800ms
- [ ] Route-based code splitting is implemented
- [ ] Unused JavaScript is removed or deferred
- [ ] Hero image is not lazy-loaded
High Priority
- [ ] All raster images converted to WebP or AVIF
- [ ] Font fallback metrics are tuned
- [ ] Third-party embeds have reserved space
- [ ] Analytics scripts load after onLoad
- [ ] Long tasks (>50ms) are identified and split
- [ ] JavaScript bundles use Brotli compression
- [ ] Static assets use immutable cache headers
- [ ] CDN is configured for all asset types
- [ ] TanStack Query staleTime is set appropriately
- [ ] Service worker caches API responses
Medium Priority
- [ ] All images use lazy loading (except LCP)
- [ ] preconnect hints for critical origins
- [ ] dns-prefetch for analytics domains
- [ ] HTML is minified
- [ ] CSS is minified, unused rules purged
- [ ] No render-blocking JS above the fold
- [ ] Preload key API routes
- [ ] Use will-change sparingly
- [ ] Minimize 301 redirects per page load
- [ ] Run Lighthouse CI on every PR
Conclusion
A Lighthouse 95+ score is about engineering a fast, reliable, and delightful user experience that directly grows your SaaS business. The approach is systematic:
- Set budgets before you write code
- Measure continuously in lab and field
- Optimize the critical path: SSR streaming, Critical CSS, font loading, images
- Control JavaScript with route splitting and deferred loading
- Cache everything aggressively
Start with the checklist. Pick the critical items first. Run Lighthouse CI on every PR. Your users — and your revenue — will thank you.
Top comments (0)