From 41 to 100 on Lighthouse in Two Days
I ran Lighthouse on TypeVelocity for the first time and the mobile score came back at 41. Forty-one. Desktop was 70, which sounds better until you realize that's still an F.
The starting point. 41 on mobile. Painful.
Desktop was 70. Still not great.
Accessibility was 83. SEO was 92. Best Practices was the only thing at 100. Two days later, everything was green.
Mobile after two days of fixes.
Desktop too. All 100s.
This is everything I did to get there. Some of it was obvious. Some of it made me question my life choices.
The CLS Disaster
CLS was 1.0. That's the maximum. The entire Performance score was tanking because of layout shift alone.
Here's what happened. TypeVelocity loads its full CSS asynchronously — the stylesheet uses the media="print" onload="this.media='all'" pattern so it doesn't block rendering. Critical styles for the header, typing area, stats bar — all that's inlined in a <style> tag so the page looks correct on first paint.
But I forgot about the hidden stuff.
Two modal overlays (results modal, confirm dialog), a cookie banner, an adblocker banner, and a tips popup — all sitting in the DOM with no inline CSS to hide them. Their hiding styles lived in the async stylesheet. So for that brief window between HTML parse and async CSS load, every single one of them was fully visible. Lighthouse saw them flash in and disappear. Layout shift: 1.0.
Five lines of inline CSS fixed it:
.modal-overlay { position: fixed; inset: 0; opacity: 0;
pointer-events: none; z-index: 100 }
.cookie-banner { position: fixed; bottom: 0;
transform: translateY(100%) }
.adblock-banner { position: fixed; top: 0;
transform: translateY(-100%) }
.tips-popup { position: fixed; opacity: 0;
pointer-events: none }
CLS dropped from 1.0 to 0.001. Performance jumped from the low 40s to 93 in one shot. I stared at the screen for a while after that one.
Accessibility: 83 to 100
The accessibility fixes were spread across a few things, but the biggest was the viewport meta tag. I had user-scalable=no which blocks pinch-to-zoom. That's a hard fail — screen readers and users with low vision need zoom. Removed it.
No <main> landmark on the page. Screen readers use landmarks to navigate, and the page had a header and footer but nothing wrapping the actual content. Added <main> around the config bar, stats, typing area, and controls.
Heading hierarchy jumped from h1 to h3, skipping h2. The typing input had no aria-label. Cookie banner link had a contrast ratio of 2.17:1 when you need 4.5:1 minimum. Decorative SVGs were missing aria-hidden="true". Each one was small but they added up.
SEO: 92 to 100
Missing meta description. A link that just said "Learn more" with no context. A Twitter meta tag using property instead of name:
<!-- wrong — Open Graph uses property, Twitter uses name -->
<meta property="twitter:card" content="summary">
<!-- correct -->
<meta name="twitter:card" content="summary">
Easy to mix up. Lighthouse catches it.
93 to 100: Killing Google Fonts
After the CLS fix, Performance was stuck at 93. FCP, LCP, and Speed Index were all hovering around 2.6 seconds on Lighthouse's simulated slow 4G connection. The metrics breakdown showed the remaining time was split between CSS parse overhead and third-party font loading.
Two things needed fixing.
First: non-composited CSS animations. Twenty-two elements had transition: background 0.35s ease — the background shorthand was transitioning background-position sub-properties that can't be GPU-composited. Changed every instance to background-color. Also removed border-color transitions from elements inside hidden modals since they only fire during theme switches while the modal is open. Nobody would ever notice.
Second: Google Fonts. We were loading JetBrains Mono and DM Sans from Google's CDN — 70KB of woff2 files, plus a CSS request, plus DNS lookups and TLS handshakes to two separate Google domains. Everything was async with font-display: optional, so technically not render-blocking. But on a throttled 1.6 Mbps connection, 70KB of font data competes for bandwidth with everything else.
I just removed them entirely.
/* the fallback stack was already there */
--font-ui: 'DM Sans', system-ui, -apple-system, sans-serif;
--font-mono: 'JetBrains Mono', 'Fira Code', 'Consolas', monospace;
Without the Google Fonts <link>, the browser drops straight to system fonts. Zero external requests. No 3rd party section in the Lighthouse report at all. The visual difference? Minimal. DM Sans and system-ui are close enough. JetBrains Mono vs Consolas is more noticeable if you're looking, but for a typing test it works fine.
That got us to 100.
The Other Stuff
Some of these were from earlier rounds before the two-day push, some came up along the way:
AdSense removal. The ad script alone was 164KB of JavaScript, third-party cookies, and forced reflows. Commenting it out took Best Practices from 77 to 100 in earlier rounds. When ads come back, they'll load lazily — injected via JavaScript on first user interaction instead of a static script tag. Lighthouse measures initial page load, not what happens after you start typing.
Async CSS. The full stylesheet uses media="print" onload="this.media='all'". Browser doesn't block rendering for it. Critical styles inlined in HTML so the page looks right immediately.
Cookie banner timing. 3.5-second setTimeout before showing it. Without the delay, Lighthouse treats the banner text as the LCP element because it's the largest visible text on initial render.
Footer deferral. Added content-visibility: auto to the footer so the browser skips rendering it until you scroll down. One less thing competing for first paint.
Modal containment. Added contain: layout style paint to modal overlays so the browser knows it can skip their layout calculations entirely when they're hidden.
What Actually Moved the Needle
If I had to rank every fix by how many points it was worth:
The CLS fix — hiding modals and banners in inline CSS — was worth roughly 50 points on mobile. That single change was the difference between a 41 and a 93. I cannot stress this enough: if you're loading CSS asynchronously and you have hidden elements in the DOM, make sure they're hidden before your async stylesheet loads.
Removing AdSense was worth about 30 points across Performance and Best Practices combined.
The accessibility fixes (viewport zoom, landmarks, aria labels, contrast) bridged the gap from 83 to 100 on that category.
Killing Google Fonts was worth the last 7 points on Performance. That's a real tradeoff — custom fonts vs a perfect score. I chose the score.
Everything else — the border-color transitions, the content-visibility on the footer, the containment on overlays — maybe a point or two combined. Marginal stuff that only matters when you're chasing perfection.
Was It Worth It?
Honestly? The jump from 41 to 93 took about three hours of actual work. Finding the CLS bug, adding inline CSS, fixing accessibility issues. High impact, low effort.
Going from 93 to 100 took the rest of the two days. Auditing every CSS transition, removing Google Fonts, adding containment hints, deferring the footer. Each fix was worth a point or two at most. Classic diminishing returns.
But the 100 is clean. And now I know exactly what costs what. If I ever re-enable ads or bring custom fonts back, I'll know the exact price in Lighthouse points. That's worth something.
TypeVelocity runs at 100/100 across the board. Come type.
Start Typing