CSS and JavaScript Optimization

Keyboard shortcuts
  • JNext lesson
  • KPrevious lesson
  • /Search lessons
  • EscClear search

Your WordPress site loads 15-40 CSS and JavaScript files on every page. Most of them aren’t needed. Some of them block your page from rendering at all. And a few of them come from third-party servers you don’t control.

I’ve audited hundreds of WordPress sites, and CSS/JS bloat is the number one problem I find after hosting. It’s not images. It’s not the database. It’s render-blocking stylesheets and JavaScript files that load before the browser can even start painting pixels on screen.

The good news? This is fixable. And the results are often dramatic. I’ve cut LCP (Largest Contentful Paint) by 1-2 seconds just by cleaning up CSS and JS delivery on client sites. No hosting changes. No redesign. Just smarter loading.

Why CSS and JS Are the Silent Killers

When your browser loads a web page, it reads the HTML from top to bottom. When it hits a <link rel="stylesheet"> tag, it stops everything and downloads that CSS file before continuing. Same with a <script> tag (unless it has defer or async). This is called render-blocking.

Your visitor is staring at a white screen while the browser downloads and processes files they might not even need for the current page.

A typical WordPress site with a page builder, a forms plugin, a slider, and a few other plugins might load 8-12 stylesheets and 10-15 JavaScript files. The page builder’s CSS loads on every page, even pages that don’t use the builder. The slider’s JavaScript loads on every page, even though the slider is only on the homepage. The forms plugin CSS loads on every page, but you only have forms on two pages.

This is what bloat looks like. Not one massive file, but dozens of small-to-medium files that collectively add 500KB-2MB of code the page doesn’t need. Each file requires a network request, parsing time, and execution time. It adds up fast.

Critical CSS: The Render Speed Secret

Critical CSS is the minimum CSS needed to render the visible portion of your page (the “above-the-fold” content). Instead of loading your entire 200KB stylesheet before the browser can paint anything, you inline just the 10-15KB of CSS needed for what the visitor sees first. The rest loads afterward, out of the way.

The impact is significant. I’ve seen First Contentful Paint improve by 0.5-1.5 seconds just from proper critical CSS implementation. The page starts rendering almost immediately because the browser doesn’t wait for stylesheets it doesn’t need yet.

How to Generate Critical CSS

You have two options: automated or manual.

Automated (recommended): FlyingPress and WP Rocket both generate critical CSS automatically. They visit each page, figure out what CSS is needed above the fold, inline it in the <head>, and defer the rest. FlyingPress does this better in my testing. It’s more accurate and handles edge cases (like sliders that change height) more reliably.

Manual: Tools like CriticalCSS.com or the npm package critical can generate critical CSS. You paste in your URL, it returns the critical CSS, and you inline it manually. This works but doesn’t scale. Every time your design changes, you need to regenerate. Not practical for most sites.

My recommendation? Let FlyingPress handle it. The automation is good enough for 95% of sites. If you’re running a high-traffic site where every millisecond counts, you can fine-tune the output, but most sites don’t need that level of control.

When Critical CSS Goes Wrong

Critical CSS generation isn’t perfect. Common issues:

Flash of unstyled content (FOUC). If the critical CSS doesn’t include styles for an element that’s above the fold, that element appears unstyled for a moment before the full CSS loads. Fix: check your above-the-fold content after enabling critical CSS. If something looks off, your plugin might need to include more CSS. FlyingPress lets you add selectors to always include.

Fonts not loading. If your font-face declarations aren’t in the critical CSS, you might see a flash of system fonts. Preloading your font files separately fixes this.

Dynamic content shifting. If your above-the-fold content changes (like a rotating slider), the critical CSS might not cover all states. Personally, I don’t use sliders. They’re slow by nature. But if you must, make sure the slider’s CSS is included in the critical set.

CSS Minification and Combination

Minification removes whitespace, comments, and unnecessary characters from CSS files. A 100KB file becomes 70-80KB. It’s free speed. Every caching plugin does this. Turn it on.

Combination merges multiple CSS files into one. This used to be a no-brainer. Ten files meant ten HTTP requests, and each request had overhead. Combining them into one file meant one request. Faster.

But HTTP/2 changed the rules.

HTTP/2 Changed Everything

HTTP/1.1 loaded files one at a time (or 6 at a time, depending on the browser). Ten files meant waiting for each one sequentially. Combining was a must.

HTTP/2 multiplexes. It loads many files simultaneously over a single connection. Ten small files load almost as fast as one combined file. Sometimes faster, because the browser can start using the first small file while the others are still downloading.

So should you combine CSS files? My answer: it depends on your server.

If you’re on HTTP/2 (most modern hosts): Skip combination. Minify only. Individual files let the browser cache them separately. If you update one file, visitors only re-download that one, not the entire combined bundle.

If you’re somehow still on HTTP/1.1: Combine. But also, switch to a host that supports HTTP/2. It’s been standard since 2015.

You can check your HTTP version in Chrome DevTools. Go to Network tab, right-click the column headers, enable “Protocol.” You’ll see “h2” for HTTP/2 or “http/1.1.”

FlyingPress minifies CSS by default and skips combination on HTTP/2 servers. Smart default behavior.

JavaScript Defer vs Async vs Module

JavaScript loading has three modes, and using the wrong one breaks things.

Default (no attribute). The browser stops parsing HTML, downloads the JS file, executes it, then continues parsing. This is render-blocking. Bad for performance.

Async (<script async>). The browser downloads the file while continuing to parse HTML, but pauses to execute it as soon as it’s downloaded. The problem: if the script depends on another script that hasn’t loaded yet, it breaks. Async is unpredictable for scripts with dependencies.

Defer (<script defer>). The browser downloads the file while parsing HTML, and executes it after the HTML is fully parsed, in order. This is almost always what you want. Scripts load in parallel, execute in order, and don’t block rendering.

Module (<script type="module"). Modern JS modules. They’re deferred by default. If your theme or a plugin uses ES modules, they’re already behaving correctly. You don’t need to add defer.

My Rule

Defer everything. Set your caching plugin to add defer to all JavaScript files. If something breaks (usually a poorly coded plugin that relies on inline scripts running immediately), exclude that specific file from deferral.

In FlyingPress, go to JavaScript tab > enable “Defer JavaScript.” If something breaks, add the problematic script’s URL to the exclusion list. I’ve done this hundreds of times. Usually 0-2 scripts need excluding per site.

Removing Unused CSS

This is where the big wins live. The average WordPress page loads 200-400KB of CSS but only uses 20-40% of it. The rest is styles for components, pages, and features that aren’t on the current page.

Page builders are the worst offenders. Elementor loads its entire CSS framework on every page. That’s 300KB+ of CSS on a simple blog post that uses none of it. GenerateBlocks is better about this because it only generates CSS for the blocks you actually use.

How to Find Unused CSS

Chrome Coverage tool. Open DevTools > click the three dots menu > More tools > Coverage. Reload the page. It shows you exactly how much of each CSS file is actually used. Red means unused. I’ve seen files where 90% is red.

FlyingPress “Remove Unused CSS.” This is my preferred solution. It automatically strips unused CSS per page. Each page gets only the CSS it needs. Enable it, test your site, and watch your CSS payload drop from 300KB to 40-60KB.

The Safety Net

Removing unused CSS can break things. A CSS rule that seems unused on page load might be needed when a user opens a dropdown menu, clicks a tab, or triggers a modal. FlyingPress handles this with “Load removed CSS on interaction.” The unused CSS loads the moment the user moves their mouse or touches the screen. By then, the page has already rendered, and the extra CSS arrives just in time for interactive elements.

If you notice something visually broken after enabling unused CSS removal, add the CSS selector to the safelist. FlyingPress calls this “Include selectors.” Common ones to add: navigation dropdowns, modal dialogs, and any content that appears on click/hover.

Removing Unused JavaScript

JavaScript bloat is trickier than CSS bloat because you can’t just strip unused code from a file the same way. JS files often have internal dependencies, and removing a function can break others.

The approach here is different: don’t remove code from files. Remove entire files from pages that don’t need them.

Identifying JS Bloat

Asset CleanUp (free plugin). Install it, visit any page, and it shows you every CSS and JS file loaded. You can unload files per page or per post type. Found the contact form plugin’s JS loading on every page? Unload it everywhere except the contact page.

Chrome DevTools Coverage. Same as CSS. Shows how much of each JS file is actually executed. Files with 80%+ unused code are candidates for removal.

Query Monitor. Shows which plugin is responsible for each script. Makes it easy to trace a slow script back to its source.

Common JS Offenders

Contact Form 7. Loads its CSS and JS on every page. Use Asset CleanUp to restrict it to pages with actual forms.

WooCommerce. Loads cart fragments JavaScript on every page, even non-shop pages. Add this to your theme’s functions.php to disable it on non-WooCommerce pages:

add_action( 'wp_enqueue_scripts', function() {
    if ( ! is_woocommerce() && ! is_cart() && ! is_checkout() ) {
        wp_dequeue_script( 'wc-cart-fragments' );
    }
});

Slider plugins. Load on every page. Restrict to pages with actual sliders.

Social sharing plugins. Load heavy JS for share buttons. Consider replacing with simple HTML share links that use no JavaScript at all.

The Third-Party Script Problem

Third-party scripts are the hardest performance problem to solve because you don’t control them. Google Analytics, Google Tag Manager, Facebook Pixel, live chat widgets, ad networks, embedded videos… each one adds JavaScript that loads from someone else’s server.

And they’re slow. A typical Google Analytics script adds 50-80ms to page load. Google Tag Manager adds 100-200ms. A chat widget like Intercom or Drift adds 200-400ms. Stack three or four of these, and you’ve added a full second to your page load before your own code even runs.

How to Audit Third-Party Scripts

Open Chrome DevTools > Network tab > filter by “JS.” Sort by domain. Everything that isn’t your domain is third-party. Note the size and load time of each.

Or use WebPageTest.org. Run a test and look at the waterfall chart. Third-party scripts show up as requests to different domains. The “Connection View” groups requests by domain so you can see exactly how much each third-party costs.

How to Minimize Third-Party Impact

Delay loading until interaction. This is the biggest win. Chat widgets, analytics (if you can tolerate a slight data loss for bounce visitors), and social scripts don’t need to load until the user interacts with the page. FlyingPress calls this “Delay JavaScript.” You add script URLs to the delay list, and they don’t load until the user scrolls, clicks, or moves the mouse. This can save 500ms-1 second on initial load.

Self-host what you can. Google Fonts? Self-host them (covered in Chapter 10). Google Analytics? Use a lightweight alternative like Plausible (7KB vs 45KB) or self-host the GA script using a plugin like “CAOS” (Complete Analytics Optimization Suite).

Replace heavy tools with lighter ones. Intercom (400KB+ of JS) vs. a simple contact form. Facebook Pixel (200KB) vs. server-side tracking. A YouTube embed (1MB+ of resources) vs. a lazy-loaded facade that only loads the player when clicked.

Reduce the number of scripts. Every third-party script you add needs to justify its existence with measurable value. If you can’t point to a specific business outcome that a script drives, remove it. I do this audit with clients regularly. We typically remove 2-3 scripts per site.

My Optimization Workflow

Here’s the exact order I follow when optimizing CSS and JS on a client site. The order matters because each step builds on the previous one.

Step 1: Baseline. Run a PageSpeed Insights test and a WebPageTest test. Screenshot the results. Note TTFB, FCP, LCP, and total blocking time.

Step 2: Remove unnecessary plugins. Before optimizing how scripts load, remove the ones that shouldn’t be there. Deactivate plugins you don’t need. Replace heavy plugins with lighter alternatives. This usually eliminates 2-5 scripts right away.

Step 3: Enable page caching. This should already be done from Chapter 6. If not, do it now. Caching reduces TTFB, which affects everything downstream.

Step 4: Defer all JavaScript. Enable defer in your caching plugin. Test the site. Fix any breakage by excluding specific scripts.

Step 5: Remove unused CSS. Enable unused CSS removal. Test every page template (homepage, blog post, archive, page). Fix visual issues by safelisting CSS selectors.

Step 6: Generate critical CSS. Enable critical CSS generation. Verify that above-the-fold content renders correctly without waiting for the full stylesheet.

Step 7: Delay third-party scripts. Add analytics, chat, and social scripts to the delay list. Test that they still fire correctly (check your analytics dashboard, test the chat widget).

Step 8: Unload per-page scripts. Use Asset CleanUp to remove plugin CSS/JS from pages that don’t need them. Contact form scripts only on contact pages. Slider scripts only on pages with sliders.

Step 9: Retest. Run PageSpeed Insights and WebPageTest again. Compare to baseline. You should see significant improvements in FCP, LCP, and Total Blocking Time.

I follow this order because each step is progressively more granular. Start with the big wins (caching, deferring), then get specific (unused CSS, per-page optimization). If you start with per-page optimization before caching, you’re doing detailed work on a foundation that isn’t set.

FlyingPress and WP Rocket Settings Comparison

I use FlyingPress. Many of you use WP Rocket. Both work. Here’s what to enable in each.

FlyingPress

CSS tab: Remove Unused CSS = ON. Load removed CSS on interaction = ON. Minify CSS = ON.

JavaScript tab: Defer JavaScript = ON. Delay JavaScript = ON. Add third-party scripts to the delay list (Google Analytics, Tag Manager, chat widgets, Facebook Pixel). Minify JavaScript = ON.

What to skip: Don’t enable “Combine CSS” or “Combine JS” on HTTP/2 servers. It doesn’t help and can cause issues.

WP Rocket

File Optimization > CSS: Minify CSS = ON. Remove Unused CSS = ON (this is a newer feature, test carefully). Optimize CSS Delivery = ON (this generates critical CSS).

File Optimization > JavaScript: Minify JavaScript = ON. Load JavaScript Deferred = ON. Delay JavaScript Execution = ON. Add third-party script keywords to the delay list.

What to skip: “Combine CSS/JS” is usually unnecessary on HTTP/2. “Optimize Google Fonts” in WP Rocket is OK, but self-hosting fonts (Chapter 10) is better.

What Breaks and How to Fix It

After enabling these optimizations, test your site by browsing every major page template. Click through navigation, open dropdowns, submit forms, use any interactive elements.

Broken navigation dropdowns: Usually caused by unused CSS removal stripping the dropdown styles. Add the dropdown class to the CSS safelist.

Forms not submitting: JavaScript defer might break inline form validation. Exclude the form plugin’s JS from deferral.

Sliders not working: Defer or delay breaks sliders that need to run immediately. Exclude the slider’s JS from defer. Or better yet, remove the slider.

Layout shift on load: Critical CSS might miss some above-the-fold elements. Check what’s shifting and include its CSS class in the critical CSS safelist.

Always test in an incognito window. Your logged-in browser might show different behavior because caching is bypassed for logged-in users.


Chapter Checklist

  • [ ] Understand render-blocking CSS and JS and why they slow page rendering
  • [ ] Critical CSS is enabled and generating correctly (no FOUC)
  • [ ] CSS minification is enabled
  • [ ] JavaScript defer is enabled for all scripts
  • [ ] Third-party scripts are delayed until user interaction
  • [ ] Unused CSS removal is enabled and tested across all page templates
  • [ ] Unnecessary plugins are deactivated or replaced with lighter alternatives
  • [ ] Per-page script unloading is configured (forms, sliders, etc.)
  • [ ] No double optimization (combining on HTTP/2 is usually unnecessary)
  • [ ] Before/after PageSpeed Insights scores are recorded

Chapter Exercise

Run a full CSS/JS audit on your site:

  1. Open Chrome DevTools > More tools > Coverage
  2. Load your homepage and record the unused CSS percentage for each file
  3. Identify the three largest CSS files with the most unused code
  4. Enable “Remove Unused CSS” in your caching plugin
  5. Test your homepage, a blog post, and your contact page for visual issues
  6. Open the Network tab and filter by “third-party” domains
  7. List every third-party script with its size and load time
  8. Add scripts that don’t need immediate loading to the delay list
  9. Retest with PageSpeed Insights. Compare your LCP and Total Blocking Time to before

Target: Get unused CSS per page below 50KB. Get third-party scripts delayed until interaction. Your LCP should improve by at least 0.3-0.5 seconds.

Disclaimer: This site is reader-supported. If you buy through some links, I may earn a small commission at no extra cost to you. I only recommend tools I trust and would use myself. Your support helps keep gauravtiwari.org free and focused on real-world advice. Thanks. - Gaurav Tiwari