DEV Community

rubendob
rubendob

Posted on

How I Reduced WordPress LCP from 7.7s to 2.5s

A website can feel fast when you open it from a desktop computer with a good connection, but the experience can be very different on a mid-range mobile device using 4G.

That was exactly what happened with my WordPress site.

During a performance analysis, I found a particularly bad metric:

LCP: 7.7s
TBT: 64ms
CLS: 0.01
Enter fullscreen mode Exit fullscreen mode

The Largest Contentful Paint was 7.7 seconds.

My target was clear:

7.7s → ≤ 2.5s
Enter fullscreen mode Exit fullscreen mode

Rather than installing another WordPress performance plugin and hoping for the best, I decided to investigate the complete request path:

Browser
   |
   v
CloudFront
   |
   v
Nginx
   |
   v
WordPress
Enter fullscreen mode Exit fullscreen mode

This led to several interesting findings involving images, CloudFront compression, Nginx configuration, JavaScript, caching and fonts.

1. Optimizing the background image

One of the first large resources I found was the background image:

fondo.jpg
394675 bytes
Enter fullscreen mode Exit fullscreen mode

It was a 1920x1080 JPG weighing around 395 KB.

The first optimization was straightforward: convert it to WebP.

The result:

BEFORE
fondo.jpg
~395 KB

AFTER
fondo.webp
~187 KB
Enter fullscreen mode Exit fullscreen mode

Almost half the amount of data for essentially the same visual result.

Safari confirmed the new asset was being served correctly:

Content-Type: image/webp
Content-Length: 187212
Enter fullscreen mode Exit fullscreen mode

This alone wasn't going to explain a 7.7-second LCP, but it was an obvious improvement.

2. Discovering that CloudFront wasn't compressing the main page

The next thing I wanted to understand was how much HTML was actually being transferred.

I tested it with curl:

curl -sS \
  -H 'Accept-Encoding: br, gzip' \
  -D /tmp/before-headers.txt \
  -o /dev/null \
  -w 'download=%{size_download} bytes\nttfb=%{time_starttransfer}s\ntotal=%{time_total}s\n' \
  https://www.example.com/
Enter fullscreen mode Exit fullscreen mode

The result was approximately:

download=108015 bytes
ttfb=0.399917 s
total=0.518430 s
Enter fullscreen mode Exit fullscreen mode

Around 108 KB of HTML.

More importantly, the response didn't contain:

Content-Encoding: gzip
Enter fullscreen mode Exit fullscreen mode

or:

Content-Encoding: br
Enter fullscreen mode Exit fullscreen mode

The HTML was being transferred without compression.

Checking CloudFront

My specific CloudFront cache behaviors already had compression enabled:

ordered_cache_behavior {
  path_pattern = "/wp-content/uploads/*"

  ...

  compress = true
}
Enter fullscreen mode Exit fullscreen mode

But the main behavior didn't.

I added:

default_cache_behavior {
  ...

  compress               = true
  viewer_protocol_policy = "redirect-to-https"

  min_ttl     = 0
  default_ttl = 3600
  max_ttl     = 86400
}
Enter fullscreen mode Exit fullscreen mode

After applying the Terraform change, I tested an Autoptimize CSS file.

Before compression:

~272 KB
Enter fullscreen mode Exit fullscreen mode

After compression:

content-type: text/css
content-encoding: gzip
x-cache: Miss from cloudfront

download=45685 bytes
Enter fullscreen mode Exit fullscreen mode

So:

CSS
~272 KB → ~46 KB
Enter fullscreen mode Exit fullscreen mode

That's roughly an 83% reduction.

CloudFront compression was working.

But there was still a problem.

The HTML wasn't being compressed.

3. The problem was also in Nginx

To isolate CloudFront, I tested the Elastic Beanstalk origin directly.

The response contained:

Content-Type: text/html; charset=UTF-8
Transfer-Encoding: chunked
Enter fullscreen mode Exit fullscreen mode

But still no:

Content-Encoding: gzip
Enter fullscreen mode Exit fullscreen mode

That meant the origin itself wasn't compressing dynamic WordPress HTML.

I added the usual Nginx gzip configuration:

gzip on;
gzip_vary on;
gzip_proxied any;
gzip_comp_level 5;
gzip_min_length 1024;

gzip_types
    text/plain
    text/css
    application/json
    application/javascript
    application/x-javascript
    text/xml
    application/xml
    application/xml+rss
    text/javascript
    image/svg+xml;
Enter fullscreen mode Exit fullscreen mode

But surprisingly, HTML was still not compressed.

That's when things got interesting.

4. Gzip was enabled... only for HTTP

I inspected the effective Nginx configuration:

sudo nginx -T 2>&1 | grep -nE 'listen .*443|listen .*80|server_name'
Enter fullscreen mode Exit fullscreen mode

I found separate server blocks:

listen 80 default_server;

listen 443 ssl;
server_name www.example.com;
Enter fullscreen mode Exit fullscreen mode

Then I checked where my gzip configuration was located.

It was inside:

server {
    listen 80 default_server;

    ...

    gzip on;
    gzip_vary on;
    gzip_proxied any;

    ...
}
Enter fullscreen mode Exit fullscreen mode

That was the problem.

My gzip configuration belonged only to the HTTP server block listening on port 80.

The actual application traffic was arriving through a different HTTPS server:

server {
    listen 443 ssl;
    server_name www.example.com;

    ...
}
Enter fullscreen mode Exit fullscreen mode

The HTTPS server wasn't inheriting the gzip configuration.

The fix

I moved the gzip configuration to:

.platform/nginx/conf.d/gzip.conf
Enter fullscreen mode Exit fullscreen mode

This placed it inside Nginx's global http {} context.

Conceptually:

http
 |
 +-- gzip on
 |
 +-- server :80
 |
 +-- server :443
Enter fullscreen mode Exit fullscreen mode

Both HTTP and HTTPS could now inherit the configuration.

Then:

sudo nginx -t
sudo systemctl reload nginx
Enter fullscreen mode Exit fullscreen mode

5. Verifying gzip at the origin

I tested the origin again.

This time:

HTTP/1.1 200 OK
Content-Type: text/html; charset=UTF-8
Transfer-Encoding: chunked
Vary: Accept-Encoding
Content-Encoding: gzip
Enter fullscreen mode Exit fullscreen mode

And the transferred HTML size changed dramatically:

BEFORE
~108 KB

AFTER
~22 KB
Enter fullscreen mode Exit fullscreen mode

Approximately an 80% reduction.

This was one of the most useful discoveries during the whole investigation.

Having:

gzip on;
Enter fullscreen mode Exit fullscreen mode

somewhere in your Nginx configuration does not necessarily mean gzip is enabled for the traffic your users are actually receiving.

Context matters.

6. Testing the full CloudFront path

Now I could test the complete chain again:

Browser
   |
   | Accept-Encoding: gzip
   v
CloudFront
   |
   v
Nginx
   |
   | gzip
   v
WordPress
Enter fullscreen mode Exit fullscreen mode

The first request returned:

content-encoding: gzip
vary: Accept-Encoding
x-cache: Miss from cloudfront

download=21769 bytes
Enter fullscreen mode Exit fullscreen mode

And a second request:

content-encoding: gzip
vary: Accept-Encoding
x-cache: Hit from cloudfront
age: 8

download=21769 bytes
Enter fullscreen mode Exit fullscreen mode

The full path was finally behaving as expected.

7. Deferring jQuery

Lighthouse also identified jquery.min.js as a render-blocking resource.

The interesting part wasn't its approximately 31 KB size.

The problem was that the browser needed to download and process it before continuing the initial render.

I was already using Autoptimize with its option to defer individual JavaScript files, but jQuery was explicitly excluded.

After removing only that exclusion and clearing the Autoptimize cache, both jquery-core and jquery-migrate started loading with:

defer
Enter fullscreen mode Exit fullscreen mode

without breaking the site.

Lighthouse then stopped listing jQuery under Render-blocking requests.

The measurements moved approximately from:

FCP         1.7s → 1.5s
LCP         3.6s → 3.4s
Speed Index 3.5s → 3.3s
Enter fullscreen mode Exit fullscreen mode

The improvement was relatively small and could partially fall within normal Lighthouse run-to-run variation.

The important result was that JavaScript had been removed from the critical rendering path without breaking dependencies.

8. Caching hashed assets for one year

I also found that Autoptimize-generated files were cached for only 30 days:

Cache-Control: max-age=2592000
Enter fullscreen mode Exit fullscreen mode

A typical generated file looked like:

autoptimize_b7e1ec6b8133370482e43a1431d3a757.css
Enter fullscreen mode Exit fullscreen mode

These assets contain a hash in their filename.

When the content changes, their URL changes as well.

That means they are excellent candidates for long-lived browser caching.

I added a specific Nginx rule for:

/wp-content/cache/autoptimize/
Enter fullscreen mode Exit fullscreen mode

using:

expires 1y;
Enter fullscreen mode Exit fullscreen mode

The response then became:

Cache-Control: max-age=31536000
Enter fullscreen mode Exit fullscreen mode

This improves repeat visits by avoiding unnecessary downloads of immutable assets.

9. Changing font-display: block to swap

One of the final Lighthouse findings involved the Flatsome icon font.

It was using:

font-display: block;
Enter fullscreen mode Exit fullscreen mode

With block, the browser may wait for the font before displaying content that depends on it.

I changed the behavior to:

font-display: swap;
Enter fullscreen mode Exit fullscreen mode

using a customization in the WordPress child theme instead of modifying the parent theme directly.

After deploying the change, my measurements went from:

LCP: 3.4s
Enter fullscreen mode Exit fullscreen mode

to:

LCP: 2.5s
Enter fullscreen mode Exit fullscreen mode

FCP remained around:

1.5s
Enter fullscreen mode Exit fullscreen mode

TBT:

0ms
Enter fullscreen mode Exit fullscreen mode

and CLS:

0.007
Enter fullscreen mode Exit fullscreen mode

A single Lighthouse execution doesn't prove that the entire 0.9-second improvement was caused by this change alone, but the result was encouraging.

Most importantly, the LCP finally reached my original target.

Final results

Some of the biggest improvements were easy to quantify:

Resource Before After
Background image ~395 KB JPG ~187 KB WebP
Home HTML ~108 KB ~22 KB gzip
Autoptimize CSS ~272 KB ~46 KB gzip

For the HTML:

108015 bytes
     ↓
~21788 bytes
Enter fullscreen mode Exit fullscreen mode

Around 80% less transferred data.

For CSS:

271802 bytes
     ↓
45685 bytes
Enter fullscreen mode Exit fullscreen mode

Around an 83% reduction.

And, most importantly:

Initial LCP
7.7s

      ↓

Final Lighthouse LCP
2.5s
Enter fullscreen mode Exit fullscreen mode

A note about TTFB

During testing I saw one request with a TTFB above six seconds.

It would have been easy to conclude that the origin server had a serious performance problem.

Instead, I repeated several forced cache-miss requests:

MISS 1  TTFB=1.648s
MISS 2  TTFB=0.831s
MISS 3  TTFB=0.858s
MISS 4  TTFB=0.674s
MISS 5  TTFB=0.739s
Enter fullscreen mode Exit fullscreen mode

The six-second request was clearly an outlier rather than normal server behavior.

This is a useful reminder when doing performance work:

don't optimize based on a single measurement.

Repeat your tests.

What I learned

My initial goal was simply to improve a very poor LCP score.

But the most interesting part of the investigation was that there wasn't one magical fix.

The final improvement came from several layers:

Heavy JPG
    ↓
WebP

Uncompressed responses
    ↓
gzip

gzip in the wrong Nginx context
    ↓
global http{} configuration

Render-blocking jQuery
    ↓
defer

Short-lived hashed assets
    ↓
1-year cache

font-display: block
    ↓
font-display: swap
Enter fullscreen mode Exit fullscreen mode

One detail was particularly easy to miss:

Enabling gzip in Nginx doesn't necessarily mean every virtual host is using it.

In my case, gzip was configured inside the port 80 server block while the real traffic was being served through the independent HTTPS server on port 443.

Moving gzip to the global configuration fixed the issue.

Performance work often ends up being less about installing another optimization plugin and more about understanding where the browser is actually spending its time.

The improvements were also reflected in external performance tools. In Wakaris, the site's performance score increased from 33% to 83%, while accessibility improved from 78% to 94%, SEO positioning from 78% to 89%, presence from 33% to 100%, and security from 63% to 82%.

The changes were also clearly visible in Google PageSpeed Insights, where the desktop version reached a 99/100 performance score, while the mobile version also showed a well-optimized result. These measurements confirmed that the work was not only reducing individual resource sizes, but improving the overall performance and quality of the site.


Originally published in Spanish on RubenOrtiz.es:

Read the original Spanish article

Top comments (0)