Still trying to hit that elusive 100 on PageSpeed Insights?
I was too.
I spent far longer than I'd like to admit stuck in the high 80s and low 90s on a real client project. I tried everything the internet recommends—compressing images, minifying JavaScript, lazy loading, removing unused CSS—and yet the score barely moved.
Mobile was the real challenge. That's where Lighthouse seems determined to humble you.
Eventually we got there on waxedoc.com, a waxing studio website I built with Astro.
Mobile PageSpeed:
- ✅ 100 Performance
- ✅ 100 Accessibility
- ✅ 100 Best Practices
- ✅ 100 SEO
(All tested in an incognito window so browser extensions weren't affecting the results.)
The interesting part is that there wasn't one magic optimization.
It was a collection of small improvements that, together, made a huge difference.
1. Start with a framework that ships less JavaScript
This almost feels unfair, but it's true.
Astro renders static HTML by default and only hydrates the components you explicitly make interactive through Islands Architecture.
For most pages on this site, the browser downloads almost no JavaScript during the initial load.
If you're building a content-heavy website, that gives you a head start before you've optimized a single line of code.
2. Stop loading fonts from Google Fonts
This change had a much bigger impact than I expected.
Using the standard Google Fonts <link> means the browser has to:
- connect to
fonts.googleapis.com - download the CSS
- connect again to
fonts.gstatic.com - finally download the font files
That's multiple network requests before your custom font even starts loading.
Instead, I downloaded the .woff2 files and hosted them locally.
@font-face {
font-family: "Playfair Display";
font-weight: 600;
font-display: swap;
src: url("/fonts/playfair-display-600.woff2") format("woff2");
}
font-display: swap is just as important—it allows text to appear immediately using a fallback font instead of staying invisible while the custom font downloads.
3. Preload only the fonts you actually use
One thing I learned is that browsers don't immediately download fonts just because they see an @font-face rule.
They first need to discover that an element actually uses that font.
Adding preload hints for fonts used above the fold helped reduce LCP.
<link
rel="preload"
as="font"
type="font/woff2"
href="/fonts/playfair-display-600.woff2"
crossorigin
/>
One warning though: only preload fonts that are genuinely needed.
I had two font files declared in CSS that weren't used anywhere. Preloading them only wasted bandwidth and triggered Lighthouse warnings about unused preloads.
4. Inline your critical CSS
External stylesheets block rendering.
Until the browser downloads and parses them, it can't paint the page.
Astro makes this surprisingly easy.
// astro.config.mjs
export default defineConfig({
build: {
inlineStylesheets: "always"
}
});
That single setting removes an extra network request before the first paint.
5. Give your LCP image real responsive images
This is one of the easiest mistakes to make.
Adding a sizes attribute by itself isn't enough.
Without a proper srcset, the browser still has only one image to choose from—which usually means your phone downloads the same massive image your desktop uses.
Astro's <Image> component makes this straightforward.
<Image
src={image}
width={image.width}
height={image.height}
widths={[480, 640, 768, 1024, image.width]}
sizes="(min-width: 1024px) 50vw, 100vw"
/>
For your Largest Contentful Paint image (usually the hero), also add:
fetchpriority="high"
That tells the browser to prioritize downloading it instead of discovering it halfway through the waterfall.
6. Configure proper cache headers
This one's easy to overlook.
Hashed assets generated during your build are immutable, so they can safely be cached for an entire year.
On Firebase Hosting I added:
"headers": [
{
"source": "**/_astro/**",
"headers": [
{
"key": "Cache-Control",
"value": "public, max-age=31536000, immutable"
}
]
}
]
It's basically free performance.
7. Don't animate box-shadow or text-shadow
This one caught me by surprise.
The animations looked perfectly smooth, but Lighthouse flagged them as non-composited animations.
Animating shadows forces the browser to repaint every frame.
A better approach is keeping the shadow static and only animating opacity or transform.
.neon {
text-shadow: 0 0 6px currentColor;
animation: flicker 3s infinite;
}
.neon::after {
content: attr(data-text);
text-shadow: 0 0 14px currentColor;
animation: flicker-glow 3s infinite;
}
Same visual effect.
Less work for the browser.
8. Avoid forced synchronous layouts
If you're repeatedly calling methods like getBoundingClientRect() during scrolling or dragging, you're probably forcing the browser to recalculate layout dozens of times every second.
Instead, read those values once and reuse them.
let box = null;
frame.addEventListener("pointerdown", () => {
box = frame.getBoundingClientRect();
});
frame.addEventListener("pointermove", () => {
if (!ticking) {
requestAnimationFrame(() => {
// use the cached box
ticking = false;
});
}
});
It doesn't sound exciting, but these little changes add up.
What actually got us to 100
There wasn't a single optimization that suddenly added 20 Lighthouse points.
It was fixing one small issue after another.
Fonts loaded a little faster.
Images became a little smaller.
Rendering started a little earlier.
JavaScript blocked the main thread a little less.
After enough of those improvements, the score finally reached 100.
More importantly, the site genuinely feels faster—not just in Lighthouse, but to real visitors.
If you'd rather see these optimizations on a real production website than a stripped-down demo:
See the actual site these optimizations shipped on—a live Astro-powered business website that consistently scores 100 on mobile PageSpeed Insights.
<a href="https://waxedoc.com" class="ltag-offer__button crayons-btn crayons-btn--primary">Visit Waxed OC</a>
I'd love to know: which Lighthouse audit has been the hardest for you to fix lately? Fonts? Images? Third-party scripts? Or something completely different?
Top comments (0)