DEV Community

Cover image for How I Pushed PageSpeed from 52 to 98 — The Lazy Loading Trap I Set for Myself
as1as
as1as

Posted on

How I Pushed PageSpeed from 52 to 98 — The Lazy Loading Trap I Set for Myself

How I Pushed PageSpeed from 52 to 98 — The Lazy Loading Trap I Set for Myself

Performance optimization has a way of humbling you.

While building TalkWith.chat, I checked PageSpeed Insights one day and saw this:

Mobile: 52. Desktop: 69.

Not great. So I started digging.


The Thing That Was Killing the Score

The biggest culprit turned out to be a single line of code.

The topic banner image — the main visual sitting above the fold, visible the moment the page opens — had loading="lazy" on it.

// The problem
<img
  src={topic.bannerImage}
  loading="lazy"  // 👈 this was it
  alt="Today's debate topic"
/>
Enter fullscreen mode Exit fullscreen mode

loading="lazy" tells the browser: "don't load this until the user scrolls near it." For images below the fold, that's a smart optimization. For the LCP element sitting at the top of the page, it's a disaster. The browser was actively deferring the most important image on the page.

The fix was one attribute:

// The fix
<img
  src={topic.bannerImage}
  fetchPriority="high"  // 👈 load this immediately
  alt="Today's debate topic"
/>
Enter fullscreen mode Exit fullscreen mode

fetchPriority="high" tells the browser this image is critical — load it first. LCP improved immediately. This single change had the biggest impact of everything I did.


The CLS Problem: Layout Jumping on Load

The second issue was CLS (Cumulative Layout Shift) at 0.147 — above the 0.1 threshold.

The cause was subtle. The banner div only rendered when an image existed:

// Before — only renders when image exists
{topic.bannerImage && (
  <div className="banner-container">
    <img src={topic.bannerImage} />
  </div>
)}
Enter fullscreen mode Exit fullscreen mode

When the page loaded before the image was ready, the banner didn't exist. When the image loaded, the banner appeared and pushed everything below it down. Classic layout shift.

The fix: always render the container, use a placeholder when there's no image:

// After — always reserves space
<div className="banner-container">
  {topic.bannerImage ? (
    <img src={topic.bannerImage} fetchPriority="high" />
  ) : (
    <div className="banner-placeholder" />
  )}
</div>
Enter fullscreen mode Exit fullscreen mode

The container now holds its space regardless of whether the image has loaded. Nothing jumps.


Image Sizing Was Also a Problem

TalkWith.chat generates a lot of images automatically:

  • At user onboarding — an AI persona image is generated based on the user's personality quiz answers
  • On level-up — the AI image evolves based on the user's debate history and comments
  • Every day, for each topic — a topic banner image, a PRO side image, and a CON side image

All of these images were coming out of the AI image generation API as 1024×1024 squares — regardless of how they'd actually be used.

A small navigation avatar doesn't need to be 1024×1024. A topic banner doesn't need to be square. Oversized images waste bandwidth and drag down performance.

I introduced proper sizing at generation time:

Use case Size
Topic banner 1024×400 (center-crop)
Pro/Con images 640×400
Persona full image 512×512
Persona avatar (nav/cards) 256×256

Then wrote two migration scripts using Pillow to backfill existing images in storage:

# resize_topic_images.py — core logic
from PIL import Image

def resize_topic_banner(img: Image.Image) -> Image.Image:
    target_w, target_h = 1024, 400

    src_ratio = img.width / img.height
    target_ratio = target_w / target_h

    if src_ratio > target_ratio:
        new_h = img.height
        new_w = int(new_h * target_ratio)
    else:
        new_w = img.width
        new_h = int(new_w / target_ratio)

    left = (img.width - new_w) // 2
    top = (img.height - new_h) // 2
    img = img.crop((left, top, left + new_w, top + new_h))

    return img.resize((target_w, target_h), Image.LANCZOS)
Enter fullscreen mode Exit fullscreen mode

Running these against existing storage brought image payload sizes down noticeably.


Final Results

Metric Before After
Mobile Performance 52 86
Desktop Performance 69 98
CLS 0.147 0.092

Desktop 98 is genuinely hard to reach. The lazy loading fix and image sizing together got there.


What I Actually Learned

Don't use loading="lazy" on above-the-fold images. Applying lazy load to everything feels like a solid optimization, but for your LCP element it actively works against you. The most important image on the page should have fetchPriority="high".

CLS isn't just about animations or fonts. Conditional rendering that adds elements after load causes layout shifts too. If a container might appear later, reserve its space from the start.

Size images at generation time, not display time. CSS can make a 1024×1024 image look small, but the browser still downloads every byte of the original. Generate the right size when the image is created.


TalkWith.chat is a daily AI debate platform — 100 AI personas argue global topics every day. talkwith.chat

Top comments (0)