One problem we’ve had on WordPress.com is we do all these amazing things and don’t tell anyone about it
My favorite example of this is the global CDN that Automattic operates in its own data centers. There are a lot of ways to measure the impact of this, but one way is authoritative DNS latency, which is on par with Cloudflare.
I’ve always liked the idea of visually differentiating my comments to make it clear when I’m replying. Some sites use a “verified” badge for this, and for a while, I did the same with an icon font. I removed the icon font along with the recent optimization work and found a way to achieve a similar verified badge effect with just a few lines of CSS—no icon font needed.
Most of the work we do as software engineers is about evolving existing systems, and for those the quality and understandability of the underlying code is crucial.
For experiments and low-stake projects where you want to explore what’s possible and build fun prototypes? Go wild! But stay aware of the very real risk that a good enough prototype often faces pressure to get pushed to production.
Indeed, most of the job is understanding and iterating on existing systems. Perhaps there is a future where code quality doesn’t matter–where code is not human readable at all. That’s more than a few years away though.
Last year, I set out to see how much I could improve my site’s performance without relying on commercial performance plugins. Almost everything I’ve implemented has been completely free. The only exception is an image resizing service I host on DigitalOcean for $5/month—though I suspect the results would be similar without it. My goal was to make these optimizations accessible to anyone.
Without compromising functionality, I’ve achieved a 97 on the Lighthouse performance score. Here’s how you can do it too.
Measuring Performance with WebPageTest and Lighthouse
Before making optimizations, it’s important to measure where your site stands. Two of the best tools for this are WebPageTest and Lighthouse.
WebPageTest
WebPageTest provides a detailed breakdown of your site’s performance, including:
Time to First Byte (TTFB): Measures server response time.
First Contentful Paint (FCP): When the first piece of content is visible.
Largest Contentful Paint (LCP): Measures how long it takes for the most important content to load.
Fully Loaded Time: The time until all page elements are completely loaded.
One of WebPageTest’s most useful features is the waterfall chart, which visually represents the sequence and timing of all network requests made by the browser. This allows you to:
Identify render-blocking resources slowing down your page.
Spot slow-loading assets, such as oversized images or unoptimized scripts.
See parallel vs. sequential loading patterns, helping you determine whether resources are efficiently loaded.
Lighthouse
Lighthouse is built into Chrome DevTools and provides insights into:
Performance: Page speed, render-blocking resources, and improvements.
Accessibility: How user-friendly the site is.
Best Practices: Ensuring up-to-date security and coding standards.
SEO: Whether your site is optimized for search engines.
To run Lighthouse:
Open Chrome DevTools (Right-click > Inspect or Cmd+Option+I / Ctrl+Shift+I).
Go to the Lighthouse tab.
Click Analyze Page Load to generate a report.
Using these tools, you can identify performance bottlenecks, apply the optimizations outlined in this guide, and then retest to measure improvements.
Pick Plugins Carefully
It may seem obvious, but it’s worth emphasizing: Be selective about the plugins you install. I almost left this section out—until I installed a popular contact form plugin and discovered it injected JavaScript and CSS on every single page, even where no forms were used. This kind of bloat slows down your site unnecessarily.
Full Page Cache
Caching HTML at the server level is easy and makes a big impact. I recommend caching HTML in Nginx, but there are many caching plugins available. Additionally, adding a CDN for static assets can further improve performance.
Defer JavaScript and CSS
Since WordPress 6.3, we have had the ability to register scripts with async or defer. Still, I don’t see many plugins taking advantage of this. I’ve been using Mark Jaquith’s encute to defer scripts (and styles!)
For reference:
async: The script is fetched in parallel with the page and executed as soon as it’s ready. Great for progressive enhancement scripts.
defer: The script loads in parallel but executes only after the document has been parsed. Ideal for analytics scripts and non-critical JavaScript.
Lazy Load Images
One of the keys ended up being to lazy load all images after the first post. By default WordPress lazy loads all images except for the first 3. I don’t post that many images. Having an image half way down the page that has fetchpriority=high was consistently impacting the FCP and LCP. This was probably more important than the image resizing service I mentioned above because it ensures the images are not loaded at all until they’re needed.
Embed Facades
Embedding YouTube videos can have a big impact. Fortunately, Paul Irish’s Lite YouTube Embed replaces the standard embed with a static image and some CSS. When the user clicks the “video” it is replaced with the actual embed. This clever optimization ensures the video is not loaded at all unless you really need it. Similar facades exist for other types of embeds as well.
Inline Critical CSS
Inlining critical CSS ensures above-the-fold content loads quickly. You can use a package like critical in your theme to extract and inline critical styles. You might as well minify your css with something like postcss and cssnano while you’re at it. To defer non-critical styles, I’ve added this snippet to my theme:
Resource hints allow browsers to anticipate and optimize how they load resources. The most useful ones for performance are:
preconnect: Establishes an early connection to an external domain to speed up subsequent requests. Ideal for CDNs and third-party services like Google Fonts.
dns-prefetch: Helps resolve domain names early to reduce latency.
prefetch: Loads low-priority resources in the background for future navigation.
prerender: Fully loads and renders an entire page in the background for seamless navigation.
WordPress has a filter to manage resource hints:Â wp_resource_hints. You can use it to add preconnect and dns-prefetch hints dynamically:
function add_resource_hints($hints, $relation_type) {
if ('preconnect' === $relation_type) {
$hints[] = 'https://fonts.googleapis.com';
}
if ('dns-prefetch' === $relation_type) {
$hints[] = 'https://cdnjs.cloudflare.com';
}
return $hints;
}
add_filter('wp_resource_hints', 'add_resource_hints', 10, 2);
Preload Web Fonts
Web fonts can significantly impact performance if not handled correctly. By preloading your primary fonts, you ensure they are available earlier in the page load process, reducing layout shifts and rendering delays.
Preloading and optimizing web fonts properly can help eliminate render-blocking issues and improve perceived performance.
Conclusion
While working on this, I noticed some claims that WordPress is inherently slower than JavaScript frameworks. This shows WordPress can be as fast as anything else.
Since WordPress 5.5, WordPress has started enabling lazy loading on images. This was dramatically improved in WordPress 6.3.
At the same time, the default value of wp_omit_loading_attr_threshold changed from 1 to 3, meaning the first 3 images will not be lazy loaded, and in fact will have fetchpriority=high. This makes sense for a typical homepage or the first blog post, but less so for the 2nd or 3rd blog post on a page.
I wrote a filter to force enable lazy loading after the first blog post. This ensures that even if the first image is farther down the page, it will not have fetchpriority=high.
// Lazy load all images after the first post
add_filter('wp_get_loading_optimization_attributes', function ($attrs, $tag, $attr, $context) {
if (is_admin() || !is_main_query()) {
return $attrs;
}
if ($tag !== 'img') {
return $attrs;
}
global $wp_query;
if ($wp_query->posts[0]->ID === get_the_ID()) {
return $attrs;
}
$attrs['loading'] = 'lazy';
$attrs['fetchpriority'] = null;
return $attrs;
}, 10, 4);