How We Fixed a Creative Agency’s Broken Core Web Vitals: A Real WP Case Study
Last month, I got an email from a boutique design shop in Brooklyn. They had a beautiful online home, but they had a massive problem. Their site took nearly seven seconds to load on a standard mobile connection.
When you run a creative shop, your work is everything. If your case studies do not load fast, prospective clients will click away before they ever see your designs. They had just spent thousands of dollars on high-resolution images and video mockups. Yet, their Google Search Console dashboard was a sea of red warnings.
I sat down with their team to look at the metrics. Their Largest Contentful Paint (LCP) was way over the line. Their layout jumped around during loading, which ruined their Cumulative Layout Shift (CLS) score.
I have spent over ten years building and fixing WordPress sites. In this post-mortem, I will walk you through exactly how we rebuilt their setup, cleaned up their database, optimized their assets, and fixed their performance issues.
Step 1: The Initial Audit and the Broken Diagnostics
Before you touch a single line of code, you have to find out where the bottlenecks are. I fired up my terminal and ran a series of raw curl tests to see the Time to First Byte (TTFB).
curl -o /dev/null -s -w "Connect: %{time_connect} TTFB: %{time_starttransfer} Total: %{time_total}\n" https://example-agency.com
The results were bad: Connect: 0.082 seconds TTFB: 1.843 seconds Total Time: 6.921 seconds
A TTFB of nearly two seconds means the server is struggling to process the PHP and run database queries before it even starts sending bytes to the visitor's browser.
Next, I logged into their database using raw MySQL commands to check the size of their options table. Overloaded options tables are one of the most common reasons WordPress sites run slow.
SELECT SUM(LENGTH(option_value)) / 1024 / 1024 AS total_mb
FROM wp_options
WHERE autoload = 'yes';
The query returned 64.2 MB.
On a healthy WordPress installation, autoloaded options should ideally be under 1 MB. This meant every single page load was forcing the database to pull 64 megabytes of settings, configurations, and transient leftovers into the server's memory. No wonder the TTFB was so high.
Step 2: Database Surgery with WP-CLI
Instead of installing a heavy database cleanup plugin that would run even more PHP cycles, I used WP-CLI directly on the server. If you have SSH access to your host, this is the safest and fastest way to clean up a database.
First, I identified which plugins were storing the most data in the options table.
SELECT option_name, length(option_value) AS option_len
FROM wp_options
WHERE autoload = 'yes'
ORDER BY option_len DESC
LIMIT 20;
The output showed thousands of old transients from a deleted social media feed plugin and massive arrays from a long-gone slider tool.
I ran the following commands to delete all expired transients:
# Delete all expired transients from the database
wp transient delete --expired
# Clean up all transients from the options table
wp db query "DELETE FROM wp_options WHERE option_name LIKE '_transient_%'"
Next, I looked for old, orphaned rows left behind by uninstalled plugins. I manually cleared out old settings rows that were still autoloading. For example, the old social slider options were deleted with:
wp option delete old_social_slider_settings
wp option delete huge_unused_plugin_data
After doing this database surgery, I ran the size query again. The autoloaded options went from 64.2 MB down to 820 KB. The server responded instantly, dropping the TTFB from 1.8 seconds down to a crisp 0.15 seconds.
Step 3: Implementing Nginx Micro-Caching
Next, we tackled the server configuration. The agency was hosted on an Nginx setup. To protect the site from dynamic traffic spikes, we set up Nginx micro-caching.
Micro-caching stores dynamic PHP pages as static HTML files for a very short period—like 5 to 10 seconds. This is perfect for portfolio sites because it keeps the server from melting down if a post goes viral on social media, but still allows updates to show up almost immediately.
I opened the Nginx configuration file:
sudo nano /etc/nginx/sites-available/agency-site
I added this helper block to configure the cache path and key zone outside the server block:
# Configure the cache path
fastcgi_cache_path /etc/nginx/cache levels=1:2 keys_zone=WORDPRESS:100m inactive=60m max_size=1g;
fastcgi_cache_key "$scheme$request_method$host$request_uri";
fastcgi_cache_use_stale error timeout invalid_header http_500;
Inside the server block, I configured the PHP processing location to use this cache:
server {
listen 443 ssl http2;
server_name example-agency.com;
# ... SSL and directory configurations ...
set $skip_cache 0;
# Post requests and urls with a query string should always go to PHP
if ($request_method = POST) {
set $skip_cache 1;
}
if ($query_string != "") {
set $skip_cache 1;
}
# Don't cache uris containing these segments
if ($request_uri ~* "/wp-admin/|/xmlrpc.php|wp-.*.php|/feed/|index.php|sitemap(_index)?.xml") {
set $skip_cache 1;
}
# Don't use the cache for logged in users
if ($http_cookie ~* "comment_author|wordpress_[a-f0-9]+|wp-postpass|wordpress_no_cache|wordpress_logged_in") {
set $skip_cache 1;
}
location ~ \.php$ {
include snippets/fastcgi-php.conf;
fastcgi_pass unix:/var/run/php/php8.2-fpm.sock;
# Enable caching
fastcgi_cache WORDPRESS;
fastcgi_cache_valid 200 301 302 10s;
fastcgi_cache_bypass $skip_cache;
fastcgi_no_cache $skip_cache;
add_header X-FastCGI-Cache $upstream_cache_status;
}
}
After checking the Nginx syntax (nginx -t) and restarting the service (systemctl restart nginx), dynamic pages were served in under 15 milliseconds for non-logged-in users.
Step 4: Rebuilding the Portfolio Presentation Layer
The agency had been using a bloated page builder that nested elements 15 divs deep just to show a single project image. This caused massive DOM sizes, which slowed down rendering on older phones.
To resolve this issue, we rebuilt their portfolio layer using a clean, well-coded base theme. We transitioned the site to the Adon WordPress Theme. This theme is optimized for creative agencies because its layouts use native CSS grids and flexbox rather than heavy JavaScript layout libraries.
Here is how we customized their portfolio list layout using a child theme file (archive-portfolio.php) to keep things exceptionally light.
<?php
/**
* Custom Portfolio Archive Template
* Theme: Adon child theme
*/
get_header(); ?>
<main id="primary" class="site-main portfolio-container">
<header class="portfolio-header">
<h1 class="page-title">Selected Case Studies</h1>
<p class="page-subtitle">Clean design meets real-world performance.</p>
</header>
<div class="portfolio-grid">
<?php if ( have_posts() ) : ?>
<?php while ( have_posts() ) : the_post(); ?>
<article id="post-<?php the_ID(); ?>" <?php post_class('portfolio-item'); ?>>
<a href="<?php the_permalink(); ?>" class="portfolio-link" aria-label="Read case study: <?php the_title_attribute(); ?>">
<div class="portfolio-image-wrapper">
<?php
if ( has_post_thumbnail() ) {
the_post_thumbnail('large', array(
'loading' => 'lazy',
'class' => 'portfolio-img',
'sizes' => '(max-width: 768px) 100vw, 50vw'
));
}
?>
</div>
<div class="portfolio-meta">
<span class="portfolio-category">
<?php
$terms = get_the_terms( get_the_ID(), 'portfolio_category' );
if ( ! empty( $terms ) && ! is_wp_error( $terms ) ) {
echo esc_html( $terms[0]->name );
}
?>
</span>
<h2 class="portfolio-title"><?php the_title(); ?></h2>
</div>
</a>
</article>
<?php endwhile; ?>
<?php else : ?>
<p class="no-results">No projects found in this collection.</p>
<?php endif; ?>
</div>
</main>
<?php get_footer(); ?>
This clean PHP file replaced 4,000 lines of page builder output with fewer than 50 lines of clean semantic HTML.
Step 5: Eliminating Layout Shifts (CLS) in the Grid
The portfolio grid suffered from bad Cumulative Layout Shift. When the page loaded, the text under the images would jump down by about 120 pixels once the images finally loaded.
To prevent this layout shift, we applied explicit aspect ratios in our stylesheet [1]. By keeping the CSS clean and lightweight, we ensured that the browser reserved the correct amount of space for the images before they actually downloaded.
/* Clean CSS Grid for Portfolio Items */
.portfolio-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
gap: 2rem;
padding: 2rem 0;
}
.portfolio-item {
display: flex;
flex-direction: column;
overflow: hidden;
}
/* Aspect ratio box to prevent Cumulative Layout Shift (CLS) */
.portfolio-image-wrapper {
position: relative;
width: 100%;
aspect-ratio: 16 / 10; /* Forces the browser to calculate the space beforehand */
background-color: #f0f0f0; /* Nice placeholder background */
overflow: hidden;
border-radius: 4px;
}
.portfolio-img {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
object-fit: cover;
transition: transform 0.3s ease-in-out;
}
.portfolio-item:hover .portfolio-img {
transform: scale(1.03);
}
.portfolio-meta {
margin-top: 1rem;
}
.portfolio-category {
font-size: 0.8rem;
text-transform: uppercase;
letter-spacing: 0.05em;
color: #666;
}
.portfolio-title {
font-size: 1.4rem;
margin: 0.25rem 0 0;
color: #111;
font-weight: 600;
}
Using aspect-ratio: 16 / 10 reserves space for the image layout on all screen sizes, bringing the CLS score down to exactly 0. You can learn more about how browsers handle shifts and how to test for them on the web.dev layout shift guide [1].
Step 6: Streamlining the Plugin Stack
Many site owners fall into the trap of installing a plugin for every single minor task. The agency had 47 active plugins. They had plugins for redirecting pages, a plugin for custom headers, a plugin to compress code, and three different analytic scripts.
Each plugin adds overhead. Some run extra files on the front end, while others load background queries that slow down the admin panel.
We audited every single tool on the site. We deleted everything that was redundant and boiled the stack down to only Essential Plugins that are strictly necessary for security, reliable contact forms, SEO structured data, and scheduled server backups.
For redirects and custom header scripts, we removed those plugins entirely and handled them at the server configuration level.
Example: Moving Redirects to Nginx
Instead of using a heavy redirection plugin that triggers a database lookup for every 404 error, we added the client's historical page moves directly into the Nginx configuration file:
# Handle old agency URLs cleanly on the server level
rewrite ^/old-about-us/$ /about/ permanent;
rewrite ^/our-work/old-project-name/$ /portfolio/new-project-name/ permanent;
rewrite ^/get-in-touch/$ /contact/ permanent;
Handling redirects directly in Nginx takes less than 1 millisecond and bypasses WordPress entirely. This protects your server from bad traffic spikes and scrapers hunting for broken URLs.
Step 7: Localizing Web Fonts
The original site loaded four separate Google Font files from the dynamic font delivery network. This forced the visitor's browser to open a secure connection to third-party servers, which added several hundred milliseconds of delay before the text could be rendered.
We downloaded the font files (specifically WOFF2 formats) and uploaded them directly to the child theme folder. We then declared them in our stylesheet using font-display: swap to prevent text layout blocks during rendering.
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 400;
font-display: swap; /* Tells the browser to show fallback text immediately */
src: url('/wp-content/themes/adon-child/fonts/inter-v12-latin-regular.woff2') format('woff2');
}
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 600;
font-display: swap;
src: url('/wp-content/themes/adon-child/fonts/inter-v12-latin-600.woff2') format('woff2');
}
body {
font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
}
This simple shift eliminated the external DNS lookup and speeded up the text display time on slower network connections.
Step 8: Automation script for media compression
Creative agencies always upload massive images. To prevent this behavior from slowing down the site in the future, we wrote a short cron-job script that automatically compresses any newly uploaded JPEG or PNG file using the server’s built-in tools.
First, we installed the necessary CLI compression tools on the server:
sudo apt-get install jpegoptim optipng
Then we wrote a small, automated script that runs once every night. It navigates to the WordPress uploads directory, finds any image uploaded in the last 24 hours, and compresses it losslessly.
#!/bin/bash
# Path to WordPress uploads folder
UPLOADS_DIR="/var/www/html/wp-content/uploads"
# Compress JPEGs uploaded in the last 24 hours
find "$UPLOADS_DIR" -type f -mtime -1 -name "*.jpg" -exec jpegoptim --max=82 --strip-all {} \;
find "$UPLOADS_DIR" -type f -mtime -1 -name "*.jpeg" -exec jpegoptim --max=82 --strip-all {} \;
# Compress PNGs uploaded in the last 24 hours
find "$UPLOADS_DIR" -type f -mtime -1 -name "*.png" -exec optipng -o2 {} \;
We saved this file as /usr/local/bin/optimize-images.sh and set a daily cron task using crontab -e:
0 2 * * * /usr/local/bin/optimize-images.sh >/dev/null 2>&1
Now, even if a designer forgets to optimize their 5MB mockup image before uploading it, the server automatically handles it at 2:00 AM without causing slow site speeds for active daytime visitors.
Real-World Performance Comparison
After applying these systematic technical optimizations, the agency’s performance metrics changed dramatically.
| Metric | Before Optimization | After Optimization | Goal | Status |
|---|---|---|---|---|
| Time to First Byte (TTFB) | 1.84s | 0.15s | < 0.8s | Passed |
| Largest Contentful Paint (LCP) | 5.2s | 1.1s | < 2.5s | Passed |
| Cumulative Layout Shift (CLS) | 0.38 | 0.00 | < 0.1 | Passed |
| Total DOM Size (Elements) | 3,420 | 580 | < 1,500 | Passed |
| Page Size | 7.2 MB | 1.4 MB | < 2.0 MB | Passed |
Key Takeaways for Your WordPress Setup
If you want to speed up your creative portfolio or high-end business site, avoid reaching for automated speed plugins that make empty promises. Instead, follow this structured optimization approach:
- Work at the server level: If your options table is bloated or your database needs attention, clean it using WP-CLI.
- Avoid page builder bloat: Rebuild critical layout structures using standard themes with a clean DOM structure.
- Use aspect ratios: Define CSS width and height expectations early to prevent irritating layout shifts [1].
- Reduce unnecessary plugins: Strip your setup down to bare-bone essentials. Run custom redirects directly inside Nginx.
- Automate your workflow: Let the server run daily tasks to compress your media folders and keep file sizes small.
Taking the time to build a robust, clean structure pays dividends. Not only will your visitors enjoy a smoother experience, but search engines will also reward your fast, stable page loads.



