4.3s
~2.5s
Mobile LCP
0.931
~0
CLS Score
288KB
94KB
Hero Image
112KB
6.6KB
Thumbnails

The Starting Point

The site is a PHP e-commerce store that's been running for over a decade. It processes millions of pounds in annual revenue, has thousands of products, and a loyal customer base. It also had a mobile PageSpeed score that Google would politely describe as "needs improvement."

The Largest Contentful Paint (LCP) was 4.3 seconds on mobile. The Cumulative Layout Shift (CLS) was 0.931 — nearly ten times the 0.1 threshold Google considers "good." The site was functional but slow, and the Core Web Vitals were actively hurting its search rankings.

Here's what I did, in the order I did it, with the reasoning behind each decision.

1. Fix the Biggest Problem First: CLS of 0.931

A CLS of 0.931 is catastrophic. The page was visibly jumping around as it loaded, and the cause was immediately obvious once I looked: CSS was loading asynchronously.

The previous developer had used a common "performance" trick — loading CSS with media="print" and swapping to media="all" on load. The idea is to prevent render-blocking CSS. In practice, it means the page renders unstyled first, then snaps into place when the stylesheet arrives. That snap is CLS.

The lesson: Not all "performance best practices" actually improve performance. Async CSS loading is fine for non-critical stylesheets (fonts, icons), but your main layout CSS must load synchronously or you'll trade a small LCP gain for a massive CLS penalty.

The fix was simple: load the main stylesheet synchronously and only defer fonts, icons, and carousel CSS. CLS dropped to near zero immediately.

2. Image Optimisation: Where the Real Savings Are

Hero images: WebP conversion

The homepage hero images were PNG files. Converting them to WebP with a quality setting of 80 gave dramatic savings:

I used the <picture> element with a WebP source and PNG fallback so older browsers still work. The WebP files were also preloaded in the <head> with fetchpriority="high" for the hero image.

Product thumbnails: right-sizing

Product thumbnails were being served at their original upload size — often 1000px+ wide — then scaled down by CSS to 400px display width. The browser was downloading 112KB per thumbnail when 6.6KB would do.

I resized thumbnails server-side to 400×400px. With dozens of products on a listing page, this saved megabytes per page load.

Batch WebP conversion tool

The store had thousands of product images. Converting them one by one wasn't practical, so I built an admin tool that:

I then modified the store's image function to automatically serve the WebP version when the browser supports it, with a transparent fallback to the original format.

3. LCP Optimisation: Telling the Browser What Matters

Even after fixing CLS and converting images, the mobile LCP was still slow. The problem was that the browser didn't know which content was most important.

The visibility:hidden anti-pattern

The site used visibility: hidden on the body tag, then relied on jQuery to show it after the page loaded. This was meant to prevent a flash of unstyled content (FOUC), but it meant the browser couldn't paint anything until jQuery loaded and executed. Both First Contentful Paint and LCP were blocked by JavaScript.

Removing this pattern and handling FOUC through synchronous CSS (which we'd already fixed) was an immediate win.

Resource prioritisation

The hero image and logo had loading="lazy" applied — exactly backwards. Above-the-fold images should load eagerly. I switched them to eager loading with fetchpriority="high" and added a preload link in the head.

For the product listing pages, I eager-loaded the first four product images and lazy-loaded the rest. This gives the user something to see immediately while the below-the-fold images load in the background.

Mobile-specific optimisation: I restricted the hero image preload to desktop viewports using a media query. On mobile, a smaller version loads naturally, saving bandwidth where it matters most.

4. Server-Side Performance

Frontend optimisation gets the most attention, but server-side changes often deliver bigger gains on PHP e-commerce sites.

Dead code removal

I found a VAT migration script that ran on every single page load, executing a database query to check if a migration had already completed. It had — years ago. Removing it saved 21ms per request. That doesn't sound like much, but over millions of page views, it adds up.

I also removed a synchronous call to an external geolocation service, duplicate analytics scripts, and dead JavaScript targeting iOS 7–9 (devices from 2013–2015 that no longer visit the site).

Query caching

The product URL generation function was hitting the database on every call, even when the same product appeared multiple times on a page. Adding a static cache to this function eliminated 24 duplicate queries per page load.

Database indexing

The product listing queries were running without composite indexes. Adding indexes on (products_status, products_date_added) and (products_status, products_ordered) gave an estimated 1.2 second improvement on category pages.

5. Caching Configuration

The .htaccess caching rules were either missing or set too conservatively. I configured:

I also disabled ETags (redundant when Expires headers are set) and enabled CSS minification, saving 44KB off the stylesheet.

6. Testing Everything

Performance work is risky on a live e-commerce site. Every change could break something — a lazy-loaded image that doesn't appear, a deferred script that breaks checkout, a cached page that shows stale prices.

I used Playwright automated tests throughout, running the full test suite after each change:

This gave me the confidence to make aggressive changes knowing the tests would catch regressions. Without automated testing, I'd have been limited to cautious, incremental changes and manual spot-checking.

The Results

All of this work was completed in a single day. The combined impact:

What I'd Do Differently

Start with CLS. I initially focused on LCP, but the CLS fix was the single biggest improvement and the simplest change. If you're working on a site with poor Core Web Vitals, check CLS first — it's often caused by one or two easily-fixable issues.

Measure before optimising. The async CSS pattern was implemented by someone who was following best practices without measuring the actual impact. Always run PageSpeed Insights before and after each change.

Build the testing first. Having Playwright tests in place before starting performance work meant I could move fast without worrying about breaking things. If I'd had to test manually, this would have taken a week instead of a day.

Is Your Store Slow?

If you're running a PHP e-commerce store — osCommerce, Drupal Commerce, WooCommerce, or a custom build — I can do the same analysis and optimisation for your site. The techniques in this article apply to any PHP-based store, and the improvements compound: faster pages mean better Google rankings, lower bounce rates, and higher conversion rates.

Get a Free Speed Check

Send me your URL and I'll run a PageSpeed analysis and tell you the three biggest improvements you can make — free, no obligation.

Get in Touch

Learn more about our PageSpeed Optimisation service →