WordPress Speed Optimization: 95 Mobile PageSpeed Score
As a WordPress developer serving European SMEs, I spend a lot of time optimizing client sites for speed. But was I practicing what I preach on my own site?
This month, I decided to audit imagewize.com and fix every performance bottleneck I could find. The results? Desktop PageSpeed jumped from 73 to 99, and mobile climbed to 95/100.
Here’s exactly how I did it—with real code examples, Core Web Vitals improvements, and technical SEO wins you can implement on your own WordPress site.
The Starting Point: PageSpeed 73 Desktop
Before diving into optimizations, I ran a baseline PageSpeed Insights audit. The results weren’t terrible, but they weren’t great either:
Before Optimization (Desktop):
- Performance: 73/100 ⚠️
- Primary Issue: Render-blocking CSS (930ms+ blocking time)
- Hero Image: 70 KiB loaded but only 576×334px displayed (~60 KiB wasted)
- LCP Resource Load Delay: 2,360ms
- CLS (Cumulative Layout Shift): 0.602 (target: <0.1)
| Issue | Impact | Technical Cause |
|---|---|---|
| Render-blocking CSS | 930ms blocking | WooCommerce CSS (16 KiB), Block Library (15.7 KiB), 15+ custom block styles loading synchronously |
| Oversized hero image | 60 KiB wasted bandwidth | Serving 1500×871px image for 576×334px display |
| LCP delay | 2,360ms | Hero image had loading="lazy" attribute on above-the-fold content |
| Layout shift (CLS 0.602) | Poor user experience | Web fonts (Open Sans) loading after initial render, causing text reflow |
For a site with PHP-FPM page caching, Redis object cache, and WebP images already enabled, these were fixable problems—not infrastructure issues.
Optimization 1 – Async CSS Loading (300-400ms Savings)
The Problem
WordPress and WooCommerce were loading ALL CSS synchronously in the <head>, even for below-the-fold content. The browser had to download and parse ALL of these before rendering anything.
The Solution
Load non-critical CSS asynchronously using the “print media” technique. I added this filter to my theme’s app/filters.php:
/**
* Make non-critical stylesheets non-render-blocking
* Uses the "print media" technique to load CSS asynchronously
*/
add_filter('style_loader_tag', function ($html, $handle) {
// List of non-critical stylesheets to load asynchronously
$async_styles = [
// Classic WooCommerce styles
'woocommerce-layout',
'woocommerce-general',
'brands-styles',
// WooCommerce Blocks styles
'wc-blocks-style',
'wc-blocks-vendors-style',
// WordPress core block library (15.7 KiB)
'wp-block-library',
// Custom block styles (below-the-fold on homepage)
'imagewize-feature-list-grid-style',
'imagewize-testimonial-grid-style',
'imagewize-carousel-style',
// ... (15+ more block styles)
// Other
'slick-carousel',
];
if (in_array($handle, $async_styles, true)) {
// Change media to print, swap to all on load
$html = str_replace(
"media='all'",
"media='print' onload=\"this.media='all'\"",
$html
);
}
return $html;
}, 10, 2);
How It Works
- CSS loads with
media='print'(browser doesn’t block render) - JavaScript
onloadevent swaps tomedia='all'after download - CSS applies without blocking initial page paint
⚠️ Critical Warning:
Do NOT async-load stylesheets with specific media queries! For example, woocommerce-smallscreen has media='only screen and (max-width: 768px)'. If you async-load it, the technique changes media to print, then swaps to all on load, making mobile-only styles apply at ALL viewport sizes. Result: WooCommerce shop grid breaks (2 columns instead of 4 on desktop).
Impact:
wp-block-library(15.7 KiB) – Previously 200ms blocking- 15 custom block styles (~12 KiB total) – Previously 120ms each
- Combined estimated savings: ~300-400ms render-blocking time
Optimization 2 – Responsive Hero Images (~60 KiB Savings)
The Problem
PageSpeed reported my hero image (hero.webp) was:
- Served at: 1500×871 pixels (69.8 KiB)
- Displayed at: 576×334 pixels
- Wasted bandwidth: 59.5 KiB
That’s 85% of the image size going unused!
The Solution
Create custom WordPress image sizes and use wp_get_attachment_image() for automatic srcset generation.
Step 1: Register Custom Image Sizes
In app/setup.php:
// Hero block responsive images (displayed at ~576x334 on desktop)
add_image_size('hero-desktop', 600, 348, true); // 1x for standard displays
add_image_size('hero-desktop-2x', 1200, 696, true); // 2x for retina displays
Step 2: Update HeroBlock.php
// Generate responsive image HTML for desktop
$desktop_image_html = wp_get_attachment_image(
$desktop_image['ID'],
'hero-desktop',
false,
[
'class' => 'desktop-only-img object-cover w-full h-full',
'loading' => 'lazy', // We'll fix this in Optimization 3!
'sizes' => '(min-width: 1024px) 576px, 50vw',
]
);
Step 3: Regenerate Thumbnails
cd /srv/www/imagewize.com/current
wp media regenerate --only-missing --path=web/wp
| Display | Image Size Loaded | File Size (WebP) | Savings |
|---|---|---|---|
| Standard display (1x) | hero-desktop 600×348 | 17 KiB | 53 KiB saved (76%) |
| Retina display (2x) | hero-desktop-2x 1200×696 | 39 KiB | 31 KiB saved (44%) |
| Before (full size) | 1500×871 | 70 KiB | – |
Impact:
- Standard displays: 53 KiB saved (76% reduction)
- Retina displays: 31 KiB saved (44% reduction)
- Faster LCP (Largest Contentful Paint)
Optimization 3 – Hero Image Eager Loading (~2s LCP Improvement)
The Problem
PageSpeed’s LCP breakdown showed:
Resource load delay: 2,360 ms ← THIS IS THE PROBLEM
Resource load duration: 270 ms
Element render delay: 80 ms
The hero image was the LCP element but had loading="lazy", which delays loading until the browser determines if the image is in the viewport. For above-the-fold images, this adds massive delay.
The Solution
Change hero image from loading="lazy" to loading="eager" and add fetchpriority="high".
$desktop_image_html = wp_get_attachment_image(
$desktop_image['ID'],
'hero-desktop',
false,
[
'class' => 'desktop-only-img object-cover w-full h-full',
'loading' => 'eager', // Changed from 'lazy'
'fetchpriority' => 'high', // Hint browser to prioritize this image
'sizes' => '(min-width: 1024px) 576px, 50vw',
]
);
Why This Works:
loading="eager": Browser loads image immediately (default behavior)fetchpriority="high": Browser prioritizes this image over other resources- LCP element loads as fast as possible without artificial delay
Impact:
- LCP resource load delay: 2,360ms → ~100ms (eliminate lazy load penalty)
- Overall LCP improvement: ~2+ seconds
Optimization 4 – Font Preloading (CLS Fix: 0.602 → <0.1)
The Problem
PageSpeed reported CLS (Cumulative Layout Shift) of 0.602 (target is <0.1). When the Open Sans web font loaded, text reflowed and shifted the entire <main> element by 0.602 units.
The Solution
Preload critical fonts in <head> so they’re available before first paint.
Added to app/setup.php:
/**
* Preload critical fonts to prevent CLS (Cumulative Layout Shift)
*/
add_action('wp_head', function () {
$manifest_path = get_theme_file_path('public/build/manifest.json');
if (!file_exists($manifest_path)) {
return;
}
$manifest = json_decode(file_get_contents($manifest_path), true);
// Preload Open Sans regular (most used weight)
$regular_font = $manifest['resources/fonts/open-sans-v40-latin-regular.woff2'] ?? null;
if ($regular_font) {
printf(
'<link rel="preload" as="font" type="font/woff2" href="%s" crossorigin="anonymous">',
esc_url(get_theme_file_uri('public/build/' . $regular_font))
);
}
// Preload Open Sans semibold (headings)
$semibold_font = $manifest['resources/fonts/open-sans-v40-latin-600.woff2'] ?? null;
if ($semibold_font) {
printf(
'<link rel="preload" as="font" type="font/woff2" href="%s" crossorigin="anonymous">',
esc_url(get_theme_file_uri('public/build/' . $semibold_font))
);
}
}, 1);
How It Works:
<link rel="preload" as="font">tells browser to download fonts immediatelycrossorigin="anonymous"required for CORS with fonts- Fonts are available before first paint → no layout shift
- Uses Vite manifest to resolve hashed font filenames (cache-busting)
Impact:
- CLS: 0.602 → <0.1 (target achieved)
- Improved perceived performance (no text reflow on load)
Optimization 5 – Database Cleanup (608 KB → 251 KB Autoload)
The Problem
WordPress’s wp_options table had bloated autoloaded data from deactivated plugins:
| Option Name | Size | Source |
|---|---|---|
wprc_info_extension | 227 KB | Old WPML Installer plugin (2014) |
wp_installer_settings | 138 KB | Old WPML Installer settings (2014) |
Why This Matters: Autoloaded options are queried and unserialized on every WordPress page load. 608 KB of data being processed on every request = memory pressure and slower performance.
The Solution
Step 1: Audit Autoloaded Options
wp db query "SELECT option_name, LENGTH(option_value) as option_size
FROM wp_options WHERE autoload='yes'
ORDER BY option_size DESC LIMIT 10;" --path=web/wp
Step 2: Backup Database
wp db export backup_$(date +%Y%m%d_%H%M%S).sql.gz --path=web/wp
Step 3: Delete Orphaned Options
wp option delete wprc_info_extension --path=web/wp
wp option delete wp_installer_settings --path=web/wp
| Metric | Before | After | Change |
|---|---|---|---|
| Options count | 844 | 842 | -2 (-0.2%) |
| Total size | 608 KB | 251 KB | -357 KB (-58.7%) |
| Largest option | 228 KB | 18 KB | -210 KB |
Impact:
- Faster page loads (less data to query/unserialize)
- Reduced memory usage per request
- Better database performance
Optimization 6 – WordPress Memory Limits (Fix Hidden Cap)
The Problem
While optimizing, I discovered WordPress was capping its own memory usage at 40MB regardless of PHP’s 512MB limit. With WooCommerce + Acorn/Laravel, each request needs ~150-250MB. The 40MB cap was causing silent failures.
The Solution
Added to site/config/application.php:
/**
* Memory Limits
*
* WP_MEMORY_LIMIT: Memory for frontend requests (WordPress default: 40M)
* WP_MAX_MEMORY_LIMIT: Memory for admin/backend requests (WordPress default: 256M)
*
* Increased for WooCommerce + Acorn/Laravel which require more memory.
*/
Config::define('WP_MEMORY_LIMIT', '256M');
Config::define('WP_MAX_MEMORY_LIMIT', '512M');
| Setting | Purpose | Default | Our Value |
|---|---|---|---|
WP_MEMORY_LIMIT | Memory for frontend/visitor requests | 40M | 256M |
WP_MAX_MEMORY_LIMIT | Memory for admin/backend requests | 256M | 512M |
php_memory_limit | PHP-FPM’s overall limit (in Trellis) | 512M | 1024M |
Impact:
- Eliminated “memory exhausted” errors on admin pages
- WooCommerce product saves no longer fail
- Acorn cache compilation succeeds on first try
Results – Before/After Comparison
PageSpeed Scores
| Metric | Before (Desktop) | After (Desktop) | Before (Mobile) | After (Mobile) |
|---|---|---|---|---|
| Performance | 73 | 99 ✅ (+26 points) | ~85 (est.) | 95 ✅ |
Core Web Vitals
| Metric | Before | After | Target | Status |
|---|---|---|---|---|
| LCP (Largest Contentful Paint) | ~3.5s | ~1.4s | < 2.5s | ✅ PASS |
| CLS (Cumulative Layout Shift) | 0.602 | <0.1 | < 0.1 | ✅ PASS |
| INP (Interaction to Next Paint) | ~200ms | ~150ms | < 200ms | ✅ PASS |
Technical Improvements:
- Async CSS (300-400ms savings)
- Responsive images (60 KiB savings)
- Hero eager loading (~2s LCP improvement)
- Font preloading (CLS fix: 0.602 → <0.1)
- Database cleanup (58.7% autoload reduction)
- WordPress memory optimization
Lessons Learned: Technical SEO for Remote Developers
As a digital nomad developer serving European clients, here are the key takeaways from this optimization project:
1. You Don’t Need a Physical Location for Technical SEO
- I work from Jakarta/Bangkok serving Dutch, German, French, and Belgian clients
- Technical SEO (speed, schema, Core Web Vitals) is location-independent
- Remote service delivery works with proper structured data
2. PageSpeed Tools Find the Real Issues
Every optimization in this post came directly from PageSpeed Insights:
- Render-blocking CSS → Async loading
- Oversized images → Responsive images
- LCP delay → Eager loading + fetchpriority
- CLS from fonts → Font preloading
3. WordPress Memory Limits Are a Hidden Trap
- Bedrock doesn’t set
WP_MEMORY_LIMITby default - WordPress caps itself at 40MB even if PHP allows 1GB
- Must be explicitly configured in
application.php
4. Database Cleanup Matters
- 365 KB of orphaned plugin data = memory pressure on every request
- Old WPML/plugin data can linger for years
- Quarterly database audits should be standard practice
Next Steps: Complete Technical SEO Strategy
This post covered speed optimization and Core Web Vitals. Here’s what’s next for imagewize.com:
- Article Schema: Add structured data to 310 blog posts
- Service Schema: Implement on WordPress Dev, WooCommerce, Speed Optimization pages
- Person Schema: Add founder information (Jasper Frumau as technical expert)
- Ongoing Monitoring: Track Core Web Vitals, schema validation, performance trends
Want Help Optimizing Your WordPress Site?
I implement these exact strategies for European SMEs and startups. If you’re experiencing:
- PageSpeed scores below 90
- Core Web Vitals failures
- Slow admin panel or WooCommerce pages
- Missing or incorrect schema markup
- High bounce rates from slow loading
Get a free technical SEO audit → Contact Us
Conclusion
WordPress speed optimization isn’t about installing a caching plugin and hoping for the best. It’s about:
- Measuring – Use PageSpeed Insights to find actual bottlenecks
- Fixing – Implement code-level solutions (not just plugin checkboxes)
- Verifying – Test with real tools and monitor Core Web Vitals
The six optimizations in this post took imagewize.com from 73 to 99 desktop PageSpeed and 95 mobile:
- Async CSS loading (300-400ms savings)
- Responsive hero images (60 KiB savings)
- Hero eager loading + fetchpriority (~2s LCP improvement)
- Font preloading (CLS: 0.602 → <0.1)
- Database cleanup (58.7% autoload reduction)
- WordPress memory limits (256M/512M)
Total effort: ~10-12 hours over November 2025
Total cost: $0 (just time and technical knowledge)
I’m tracking imagewize.com’s technical SEO performance over the next 6 months. Follow the Speed Optimization category for progress updates on rankings, traffic, and conversions.
Transparency Note
All code examples in this post are from imagewize.com’s actual implementation in November 2025. You can see the results yourself:
- Mobile PageSpeed: https://pagespeed.web.dev/analysis/https-imagewize-com/lhynn9nd6g?form_factor=mobile (95/100)
- Desktop PageSpeed: https://pagespeed.web.dev/analysis/https-imagewize-com/lhynn9nd6g?form_factor=desktop (99/100)