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"
/>
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"
/>
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>
)}
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>
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)
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)