Développement

PageSpeed went from 54 to 91 in one day — and none of the real causes were “compress your images”

My site scored a 54 on PageSpeed Insights. That same evening: 91, with accessibility and best practices both at 100. The four real causes aren't listed on any generic checklist—and one of them was my browser.

4 juillet 202610 min de lecturePASCAL POTVIN
Écouter l'article

The Report That Stings (and Why It Was Only Half-True)

This morning, PageSpeed Insights gave my own site a score of 54 for mobile performance. For someone who sells “high-performance” websites, this is the kind of number you’d rather discover yourself before a client does. That same evening, the same test showed a score of 91 for performance, 100 for accessibility, and 100 for best practices. One day’s work, four real causes—and I’ll warn you right away: none of them were “compress your images,” “enable caching,” or the other refrains from checklists that have been recycled since 2019.

But before fixing anything, I had to correct the measurement itself. There were two pitfalls in the initial report. One: I was testing pascalpotvin.com instead of www.pascalpotvin.com—the 301 redirect to the canonical domain was taking 780 ms on the stopwatch before even the first byte of the page loaded. Always test the final URL, not the one that redirects. Two: my “best practices” score of 54 came almost entirely… from my Chrome extensions. The report listed console errors from a domain that my site never calls and about 14 MB of JavaScript injected by extensions—crypto wallets, screen capture tools, ad blockers. Lighthouse actually states this explicitly in its warnings—which nobody reads—that extensions skew the audit. In incognito mode, the same page scored 100. First lesson, free of charge: before you “fix” your site, make sure the problem isn’t your browser.

Pitfall #1: Your content doesn’t exist until hydration

The real culprit behind the 54 score was indeed in my code. The LCP—Largest Contentful Paint, the moment when the largest visible element finishes rendering—was stuck at 6.6 seconds, and the report details showed a “render delay” of 91%. Translation: the HTML loaded quickly, but the element remained invisible for seconds. Why? My sections animated as they entered the viewport.

The classic pattern: an AnimatedSection component that renders its content with opacity: 0, waits for the IntersectionObserver to detect it, then animates to opacity: 1. Elegant on screen, disastrous during loading: on the server side, the initial state is “hidden,” so the delivered HTML contains the entire page… invisible. Nothing is displayed until React hydrates and the observer fires—on a simulated slow mobile device, this delays the first actual render by several seconds. The fix boils down to one idea: the server renders the content visible, and it’s the client that decides, after assembly, to hide only what’s below the fold to animate it on scroll.

js
// useInView — three phases instead of two
const [phase, setPhase] = useState("ssr"); // SSR: visible, no animation

useEffect(() => {
  const el = ref.current;
  // already in the initial viewport (hero): stay visible, do not animate
  if (el.getBoundingClientRect().top < window.innerHeight) return;
  setPhase("hidden"); // below the fold: hidden, will animate on entry
  // … IntersectionObserver → setPhase("visible")
}, []);

The visual effect on scroll remains intact. The difference: a visitor (or a bot) loading the page sees the content immediately. This single fix reduced the LCP from 6.6 to 4.4 seconds. If your site uses Framer Motion, AOS, or any “reveal on scroll” library, check your server-side HTML: if your hero section is set to opacity: 0 there, you’re paying exactly this penalty.

Pitfall #2: A fading headline doesn’t “count”

Second discovery, more counterintuitive. After the first fix, the LCP remained stuck at around 4.3 seconds, attributed to an unlikely element: a number in my statistics section, barely visible at the bottom of the viewport. My actual title—the big h1 in the hero section—wasn’t even a candidate. The reason: it appeared as a fade-in, via a CSS animation that starts at opacity: 0 with a delay of 250 ms.

Here’s a rule that almost no one knows: for LCP, an element with zero opacity isn’t “painted”. As long as your title fades to black, the browser considers it nonexistent, and the metric latches onto the largest element that’s actually visible—in my case, a measly “20+” from the stats section, rendered late. The compromise I settled on: the h1 slides in without fading (using only a transform animation, which doesn’t affect the LCP), and the rest of the hero section retains its cascade of fades. Result measured under actual throttling conditions: LCP at 1.7 seconds, equal to the FCP, attributed to the correct element. In a test without throttling, 116 milliseconds.

.The animation rule that saves your LCP

transform animations and opacity animations aren’t treated equally by Core Web Vitals. An element that slides is rendered from the very first frame; an element that fades doesn’t exist until the fade is complete. Reserve fade-in animations for secondary elements—never for the text block or the largest image in your viewport.

Pitfall #3: Fonts You Don’t Preload Will Repaint

The third layer of the onion. My site loads three font families: Geist for headings, Geist Mono for technical labels, and Newsreader for editorial italics. In a premature optimization effort, I had set preload: false on the latter two—“they’re secondary,” I thought. Except that the Newsreader italics are in the h1, and the mono labels are everywhere above the fold. These fonts would load late; the browser would repaint the affected blocks upon their arrival, and each late repaint would push back the LCP and create a micro-shift in the layout.

The fix is one line per font in next/font: preload: true for everything that appears above the fold. Cost: two woff2 files (~30 KB) requested earlier. Benefit: no more late repaints, and the layout shift in my stats section has disappeared from the report. The general rule: if a font is visible on the first screen, it is preloaded. “Secondary font” is a concept of visual hierarchy, not loading.

Pitfall #4: 534 lines of JavaScript for a static page

The final step, and the most transformative: my homepage was a single client-side component spanning 534 lines—hero, services, portfolio, blog, contact—all hydrated on the browser side, even though 90% of that content never changes and has no interactivity. This is the default flaw of many Next.js sites: a "use client" at the top of the file added on a busy day, and the entire page gets bundled.

Splitting it into server-side components took me an hour: the content is rendered on the server, and only three small sections remain client-side—scrolling to anchors, the blog section filters, and the lazily loaded contact form. The Total Blocking Time dropped from 290 to 120 ms, and the delivered HTML now contains all the content, which benefits more than just performance: AI crawlers that don’t execute JavaScript can now read the entire page—I explained why this matters in my article on AEO.

What “91” Means (and Doesn’t Mean)

A word of honesty about the numbers, because the industry perpetuates confusion. The PageSpeed “lab” score is a simulation: Lighthouse loads your page and then models a mid-range mobile device on a slow network (“Lantern” mode), and this model is notoriously pessimistic for pages that include JavaScript—my simulated LCP still shows 3.5 s, whereas the actual throttled measurement is 1.7 s and the real-world measurement is 0.2 s. Scores also fluctuate from one test to the next: my first run after applying fixes scored 75, due to a first byte time of 745 ms on a cold start; the second scored 91. A score isn’t a physical constant—it’s a sample.

What matters for your SEO are the real-world Core Web Vitals, measured on your actual Chrome visitors: LCP under 2.5 seconds, INP under 200 ms (the interactivity metric that replaced FID on March 12, 2024), and CLS under 0.1—at the 75th percentile. In May 2026, 55.9% of the domains tracked by the Chrome UX Report met all three criteria. If your site is small, PageSpeed will display “insufficient data” in this section: it’s not a bug, just a lack of traffic—and one more reason not to live and die by the lab score.

My Method, Step by Step

If your score is poor, here’s the order of investigation that worked for me—it’s a decision tree, not a checklist. First, ensure the measurement is reliable: use a canonical URL without redirects, incognito mode, or a hosted tool, and run the test twice. Next, open the LCP details: which element and which phase is dominating? A massive “render delay” points to content hidden by JavaScript or an opacity animation—not to your images. Then check your fonts: is everything above the fold preloaded? Only then should you tackle the JavaScript: anything that isn’t interactive has no place in a client-side component. Images, compression, caching? Check them, of course—but in 2026, with a modern framework and a CDN, that’s almost never where a 54 is hiding.

Sources

§ COMMENTAIRES

Laisser un commentaire