Page speed affects bounce rate, conversion, and search ranking. A slow site loses visitors before they see your content. I've watched support tickets pile up because a single unoptimized hero image added three seconds to load time.
The good news? Most speed wins come from a handful of proven techniques. You don't need to rewrite your stack or hire a performance consultant. Image compression, CDN delivery, minification, lazy loading, and proper caching headers fix the majority of slow sites I see in production.
Compress images before upload
Images account for the largest share of page weight on most sites. A single uncompressed screenshot can weigh more than your entire HTML, CSS, and JavaScript combined.
Compress images before you upload them. Tools like ImageOptim, TinyPNG, or Squoosh strip metadata and reduce file size without visible quality loss. For batch processing on the server, use something like jpegoptim or optipng.
jpegoptim --size=200k *.jpg
optipng -o5 *.png
Those commands compress JPEGs to a target file size and optimize PNGs for smallest output. Run them in your upload directory or build pipeline.
Serve WebP or AVIF formats to browsers that support them. These modern formats deliver the same visual quality at half the file size. Your image CDN or WordPress plugin can handle automatic format negotiation based on the Accept header.
Choose the right format
JPEG for photos. PNG for graphics with transparency. SVG for logos and icons. WebP as a drop-in replacement for both JPEG and PNG when browser support allows it.
Don't serve a 3000×2000 pixel image when the container is 600 pixels wide. Use responsive image markup with srcset so mobile users download smaller files:
<img src="hero-600.jpg"
srcset="hero-600.jpg 600w,
hero-1200.jpg 1200w,
hero-2400.jpg 2400w"
sizes="(max-width: 600px) 100vw, 600px"
alt="Dashboard screenshot">
The browser picks the closest match. A phone on 3G downloads the 600-wide version. A desktop with a 4K monitor can fetch the 2400-wide image if needed.
Set up a CDN
A content delivery network caches your static files on servers distributed around the world. When someone in Tokyo visits your site hosted in Virginia, they download CSS, images, and JavaScript from a nearby edge server instead of crossing the Pacific.
Cloudflare is the fastest free option to set up. Point your domain's nameservers to Cloudflare, enable the orange cloud on your DNS records, and static assets are automatically cached at the edge. Paid CDNs like BunnyCDN or KeyCDN give you more control over cache rules and purge behavior.
For WordPress, use a plugin like WP Rocket or W3 Total Cache to rewrite asset URLs so they point to your CDN hostname. If you're running a custom app, update your asset helper to prepend the CDN domain:
const CDN_URL = 'https://cdn.example.com';
const assetPath = (path) => `${CDN_URL}${path}`;
Now every image, stylesheet, and script loads from the edge. First-byte time drops because the CDN terminates SSL closer to the user and serves cached content without hitting your origin server.
CDN cache headers matter
Set long cache lifetimes for assets with fingerprinted filenames like app.a3f2b1.css. These files never change once deployed, so you can cache them for a year:
Cache-Control: public, max-age=31536000, immutable
For assets without fingerprints, use a shorter TTL and rely on ETag or Last-Modified headers so browsers can revalidate. The CDN respects your origin headers unless you override them in CDN config.
Minify CSS and JavaScript
Minification strips whitespace, comments, and unnecessary characters from code. A 120 KB stylesheet often shrinks to 90 KB after minification. That's 30 KB less to download and parse.
Modern build tools handle this automatically. Webpack, Vite, and Parcel minify in production mode by default. If you're serving unprocessed files, run them through a minifier before deployment:
npx terser input.js -o output.min.js
npx csso input.css -o output.min.css
For WordPress, caching plugins like Autoptimize or WP Rocket concatenate and minify CSS and JavaScript on the fly. Enable those features in plugin settings and test the output. Occasionally a plugin breaks when scripts are concatenated in the wrong order, so check console errors after enabling minification.
Inline critical CSS
The browser can't render the page until it downloads and parses your main stylesheet. For above-the-fold content, inline the critical CSS directly in the HTML <head> so the first paint happens without waiting for a separate request.
Extract critical CSS with a tool like critical or Penthouse, then embed it in a <style> tag. Load the full stylesheet asynchronously afterward:
<style>
/* critical CSS for hero and header */
</style>
<link rel="preload" href="/css/main.css" as="style" onload="this.onload=null;this.rel='stylesheet'">
This technique shaves a second off perceived load time on slower connections. The user sees content immediately while the rest of the CSS loads in the background.
Lazy-load images below the fold
Don't make the browser download every image on page load. Images below the fold can wait until the user scrolls down. Lazy loading defers those requests and speeds up the initial render.
Modern browsers support native lazy loading with a single attribute:
<img src="photo.jpg" loading="lazy" alt="Office view">
The browser loads the image only when it's about to enter the viewport. No JavaScript required. For older browsers, polyfill with a library like lazysizes or vanilla-lazyload.
WordPress added native lazy loading to images in version 5.5, so if you're running a recent release it's already active. Check the rendered HTML to confirm the loading="lazy" attribute is present.
Lazy-load iframes too
Embedded YouTube videos, Google Maps, and third-party widgets are heavy. Each iframe pulls in a cascade of requests. Lazy-load them the same way:
<iframe src="https://www.youtube.com/embed/VIDEO_ID" loading="lazy"></iframe>
Or replace the iframe with a thumbnail until the user clicks play. Libraries like lite-youtube-embed do this for YouTube embeds, cutting initial page weight by hundreds of kilobytes.
Configure browser caching headers
Browsers cache files locally so repeat visitors don't re-download everything. You control cache behavior with HTTP headers. Set long lifetimes for static assets and short lifetimes for HTML.
In Apache, add this to .htaccess:
<IfModule mod_expires.c>
ExpiresActive On
ExpiresByType image/jpg "access plus 1 year"
ExpiresByType image/jpeg "access plus 1 year"
ExpiresByType image/png "access plus 1 year"
ExpiresByType image/webp "access plus 1 year"
ExpiresByType text/css "access plus 1 month"
ExpiresByType application/javascript "access plus 1 month"
ExpiresByType text/html "access plus 0 seconds"
</IfModule>
For Nginx, put this in your server block:
location ~* \.(jpg|jpeg|png|webp|css|js)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}
location = /index.html {
expires -1;
add_header Cache-Control "no-cache";
}
HTML gets no-cache so the browser revalidates it on every visit. Images and stylesheets get a year-long cache because their filenames change when content changes.
Use ETags for validation
An ETag is a fingerprint of the file. When the browser requests a cached file, it sends the ETag in an If-None-Match header. If the file hasn't changed, the server responds with 304 Not Modified and zero bytes transferred. The browser uses its cached copy.
Most servers generate ETags by default. Check your response headers for ETag: "5f8a3b2c". If it's missing, enable FileETag in Apache or ensure Nginx isn't stripping it.
Enable Gzip or Brotli compression
Text files like HTML, CSS, and JavaScript compress well. Gzip cuts their size by 70-80%. Brotli does even better, but Gzip is universally supported.
In Apache, enable mod_deflate:
<IfModule mod_deflate.c>
AddOutputFilterByType DEFLATE text/html text/css application/javascript application/json
</IfModule>
In Nginx:
gzip on;
gzip_types text/css application/javascript application/json;
gzip_min_length 1000;
Don't compress images or video. They're already compressed and Gzip makes them slightly larger. Target text-based MIME types only.
If your CDN supports Brotli, enable it. Cloudflare serves Brotli automatically when the client sends Accept-Encoding: br. You don't need to configure anything on your origin.
Reduce DNS lookups and third-party requests
Every external domain adds a DNS lookup, TCP handshake, and SSL negotiation. If your page loads fonts from Google, analytics from three different vendors, ads from a network, and chat widgets from a SaaS tool, you're paying that penalty six times.
Consolidate third-party scripts where possible. Self-host Google Fonts instead of loading them from fonts.googleapis.com. Move analytics to a single provider. Remove widgets you don't actually use.
For scripts you must load externally, use dns-prefetch or preconnect in the <head> so the browser resolves DNS and opens connections early:
<link rel="dns-prefetch" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://cdn.example.com" crossorigin>
preconnect is more aggressive and does the full handshake. Use it for domains you're certain the page will request. Use dns-prefetch for others.
So what if you've done all this and the page is still slow? Look at server response time next. A slow database query or unoptimized PHP can add seconds to time-to-first-byte even if your assets are perfect.
Frequently asked questions
How do I measure page speed improvements?
Run Lighthouse in Chrome DevTools or use WebPageTest. Compare metrics like First Contentful Paint and Largest Contentful Paint before and after changes. Focus on the median of three runs, not a single test.
Will a CDN help if most of my traffic is local?
Yes. The CDN still offloads asset delivery from your origin server, freeing up resources for dynamic requests. You also get faster SSL termination and automatic Brotli compression.
Can I lazy-load above-the-fold images?
No. Lazy-loading above-the-fold images delays the Largest Contentful Paint and hurts perceived performance. Only lazy-load images the user can't see without scrolling.
What's the difference between minification and compression?
Minification removes unnecessary characters from source code before deployment. Compression (Gzip or Brotli) happens at request time on the server or CDN. Use both.
Do caching plugins slow down WordPress?
Good caching plugins make WordPress faster by serving static HTML instead of running PHP on every request. Badly configured plugins can cause issues, so test thoroughly after enabling cache features.
Start with the biggest wins
Images, CDN, and caching headers give you the fastest return. Compress images before upload, point your assets at a CDN, and set long cache lifetimes for static files. Those three changes alone cut load time in half on most sites.
Minification and lazy loading add incremental gains once the big stuff is handled. Inline critical CSS if you're chasing sub-second render times, but don't start there.
Measure before and after. A two-second improvement feels abstract until you see the Lighthouse score jump from 45 to 92. Then you'll know the work paid off.
