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
The Largest Contentful Paint was 7.7 seconds.
My target was clear:
7.7s → ≤ 2.5s
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
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
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
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
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/
The result was approximately:
download=108015 bytes
ttfb=0.399917 s
total=0.518430 s
Around 108 KB of HTML.
More importantly, the response didn't contain:
Content-Encoding: gzip
or:
Content-Encoding: br
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
}
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
}
After applying the Terraform change, I tested an Autoptimize CSS file.
Before compression:
~272 KB
After compression:
content-type: text/css
content-encoding: gzip
x-cache: Miss from cloudfront
download=45685 bytes
So:
CSS
~272 KB → ~46 KB
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
But still no:
Content-Encoding: gzip
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;
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'
I found separate server blocks:
listen 80 default_server;
listen 443 ssl;
server_name www.example.com;
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;
...
}
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;
...
}
The HTTPS server wasn't inheriting the gzip configuration.
The fix
I moved the gzip configuration to:
.platform/nginx/conf.d/gzip.conf
This placed it inside Nginx's global http {} context.
Conceptually:
http
|
+-- gzip on
|
+-- server :80
|
+-- server :443
Both HTTP and HTTPS could now inherit the configuration.
Then:
sudo nginx -t
sudo systemctl reload nginx
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
And the transferred HTML size changed dramatically:
BEFORE
~108 KB
AFTER
~22 KB
Approximately an 80% reduction.
This was one of the most useful discoveries during the whole investigation.
Having:
gzip on;
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
The first request returned:
content-encoding: gzip
vary: Accept-Encoding
x-cache: Miss from cloudfront
download=21769 bytes
And a second request:
content-encoding: gzip
vary: Accept-Encoding
x-cache: Hit from cloudfront
age: 8
download=21769 bytes
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
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
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
A typical generated file looked like:
autoptimize_b7e1ec6b8133370482e43a1431d3a757.css
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/
using:
expires 1y;
The response then became:
Cache-Control: max-age=31536000
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;
With block, the browser may wait for the font before displaying content that depends on it.
I changed the behavior to:
font-display: swap;
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
to:
LCP: 2.5s
FCP remained around:
1.5s
TBT:
0ms
and CLS:
0.007
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
Around 80% less transferred data.
For CSS:
271802 bytes
↓
45685 bytes
Around an 83% reduction.
And, most importantly:
Initial LCP
7.7s
↓
Final Lighthouse LCP
2.5s
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
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
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:
Top comments (0)