Remove Unused CSS and JavaScript: Dead Code Elimination Guide
Quick Summary
- What this covers: Audit and eliminate unused CSS/JavaScript through coverage analysis, tree shaking, and PurgeCSS. Reduce bundle sizes and accelerate parse/execution time.
- Who it's for: site owners and SEO practitioners
- Key takeaway: Read the first section for the core framework, then use the specific tactics that match your situation.
Unused CSS and JavaScript bloat page weight while consuming parse and execution time without delivering functionality—sites shipping 500KB stylesheets where 60% never applies to rendered elements hemorrhage bandwidth and processing overhead. Coverage analysis from Chrome DevTools reveals the scope: typical production sites serve 40-70% unused CSS and 50-80% unused JavaScript on any single page. This dead code inflates First Contentful Paint through extended parse times and degrades Total Blocking Time through unnecessary execution cycles, directly impacting Core Web Vitals scores and organic search visibility.
This guide architects systematic unused code elimination: Chrome DevTools Coverage analysis to quantify dead code, PurgeCSS configuration for automated CSS removal, tree shaking for JavaScript dead code elimination, code splitting to isolate page-specific code, and monitoring protocols to prevent unused code accumulation from returning post-optimization.
Coverage Analysis and Unused Code Detection
Chrome DevTools Coverage panel quantifies unused code with byte-level precision, revealing which portions of CSS and JavaScript files actually execute during page interactions.
Access Coverage panel: Open DevTools (F12 or Cmd+Opt+I), press Cmd+Shift+P (Windows: Ctrl+Shift+P), type "coverage", select "Show Coverage." The Coverage panel appears as a drawer tab at the bottom of DevTools.
Recording coverage:
- Click the record button (circle icon) in Coverage panel
- Reload the page (Cmd+R) to capture initial page load coverage
- Interact with the page—open menus, click buttons, scroll to reveal lazy-loaded content
- Stop recording to analyze results
The Coverage panel displays all loaded CSS and JavaScript files with usage bars. Red segments indicate unused bytes, blue segments show used bytes. The "Unused Bytes" column quantifies wasted bytes per file, while "Usage Visualization" provides visual representation of used vs unused code distribution.
Interpreting results:
- 80%+ red — Severe over-inclusion, file contains mostly dead code
- 50-80% red — Significant optimization opportunity, likely framework or library code where small portions are used
- 20-50% red — Moderate waste, possibly CSS covering multiple page templates or feature code for different sections
- <20% red — Acceptable, file is mostly utilized
Click any file in the Coverage list to open Sources panel with coverage highlighting. Red highlighting marks unused lines, blue highlighting shows executed code. This granular view identifies specific rules or functions to remove.
Framework CSS waste patterns: Bootstrap, Foundation, Tailwind (without PurgeCSS) commonly show 70-90% unused CSS. These frameworks include thousands of utility classes and components; typical pages use 10-30% of available classes. Coverage analysis exposes the magnitude:
bootstrap.min.css: 152 KB total, 121 KB unused (79.6% unused)
This indicates only 31KB of Bootstrap's CSS applies to the page—121KB serves no purpose beyond inflating page weight.
JavaScript library waste: jQuery, Lodash, Moment.js, React (without tree shaking) exhibit similar patterns:
lodash.min.js: 71 KB total, 68 KB unused (95.8% unused)
The application uses 2-3 Lodash functions, but standard imports load the entire library—68KB of functions that never execute.
Per-page coverage variation: Different page templates utilize different code portions. Homepage may use 40% of CSS, product pages 35%, blog posts 30%. No single page uses 100% of site-wide CSS. Coverage analysis on multiple page types reveals optimization targets:
- Run coverage on homepage, note unused code
- Run coverage on product page, note unused code
- Run coverage on blog post, note unused code
- Identify code unused across all three—prime deletion candidates
Manual inspection workflow: For files showing 80%+ unused code:
- Click filename in Coverage panel to open Sources view
- Scroll through red-highlighted sections
- Identify entire features, components, or utility classes unused
- Search codebase to confirm these features genuinely don't exist on any page
- Delete confirmed-unused code from source files
This manual audit applies to custom CSS/JS where automated tools (PurgeCSS, tree shaking) don't operate.
PurgeCSS Configuration for Automated CSS Cleanup
PurgeCSS scans HTML/JS files for class names, IDs, and selectors, removing CSS rules matching nothing in your content. This automated approach scales to massive stylesheets where manual auditing is impractical.
Installation:
npm install --save-dev @fullhuman/postcss-purgecss
PostCSS integration (for Webpack, Gulp, or PostCSS CLI builds):
// postcss.config.js
module.exports = {
plugins: [
require('@fullhuman/postcss-purgecss')({
content: [
'./src/**/*.html',
'./src/**/*.js',
'./src/**/*.jsx',
'./src/**/*.vue'
],
defaultExtractor: content => content.match(/[\w-/:]+(?<!:)/g) || []
})
]
};
This configuration tells PurgeCSS to scan all HTML, JavaScript, JSX, and Vue files in src/, extract class names and selectors, and remove CSS rules matching nothing found in those files.
Standalone CLI usage:
purgecss --css styles.css --content index.html about.html product.html --output dist/
PurgeCSS reads styles.css, scans the three HTML files for used selectors, and outputs purged CSS to dist/styles.css.
Tailwind CSS integration (built-in PurgeCSS):
// tailwind.config.js
module.exports = {
content: [
'./src/**/*.html',
'./src/**/*.js',
'./src/**/*.jsx'
],
theme: {},
plugins: []
};
Tailwind v3+ uses the content configuration to automatically purge unused utilities. Running npx tailwindcss -o output.css --minify generates CSS containing only utilities referenced in content files.
Safelist for dynamic classes: PurgeCSS removes classes not found literally in content files. Dynamically constructed classes escape detection:
// This pattern hides classes from PurgeCSS
const colorClass = `text-${color}-500`; // "text-blue-500" won't be detected
Safelist these patterns explicitly:
// purgecss.config.js
module.exports = {
content: ['./src/**/*.html', './src/**/*.js'],
safelist: {
standard: ['text-blue-500', 'bg-red-500'],
deep: [/^text-/, /^bg-/], // Keep all classes starting with text- or bg-
greedy: [/^btn-/] // Keep btn- classes and descendants
}
};
Safelist modes:
- standard — Exact class matches
- deep — Regex patterns matching classes and their pseudo-class variants (:hover, :focus)
- greedy — Regex patterns matching classes and all descendants
Testing after PurgeCSS: Visual regression testing catches unintended deletions. Use tools like Percy, BackstopJS, or manual QA across major page templates:
- Generate purged CSS
- Deploy to staging environment
- Systematically browse all page templates
- Check interactive states (hover effects, modals, dropdowns, form validation)
- Identify any broken styles (PurgeCSS removed something it shouldn't have)
- Add broken selectors to safelist
- Regenerate and retest
File size reductions: PurgeCSS typically achieves 70-90% size reduction on framework CSS:
Before: bootstrap.css (152 KB)
After: bootstrap.purged.css (23 KB)
Reduction: 84.9%
For custom stylesheets, reductions vary with code quality—well-maintained custom CSS may reduce 30-50%, while legacy CSS accumulating over years may reduce 70%+.
Critical CSS + PurgeCSS workflow:
- Run PurgeCSS to remove all unused CSS
- Run Critical CSS extraction on purged CSS
- Inline critical CSS
- Async load remaining purged CSS
This combines benefits: PurgeCSS eliminates dead code, Critical CSS extraction isolates above-the-fold needs, resulting in minimal inline CSS and minimal async-loaded CSS.
JavaScript Dead Code Elimination and Tree Shaking
Tree shaking removes unused JavaScript exports from ES6 modules, preventing bundlers from including functions, classes, or variables never imported by application code.
Webpack 5 tree shaking (enabled by default in production mode):
// webpack.config.js
module.exports = {
mode: 'production', // Enables tree shaking
entry: './src/index.js',
output: {
filename: 'bundle.js',
path: __dirname + '/dist'
}
};
Webpack analyzes module imports/exports, flags unused exports, then Terser minifier eliminates flagged code during minification.
ES6 module requirements: Tree shaking requires ES6 import/export syntax. CommonJS require() creates dynamic dependencies static analysis cannot reliably resolve:
// Tree-shakeable (ES6)
import { used function } from './utils';
// NOT tree-shakeable (CommonJS)
const utils = require('./utils');
const fn = utils.usedFunction;
Ensure Babel configuration preserves ES6 modules:
{
"presets": [
["@babel/preset-env", {
"modules": false // Don't transform modules
}]
]
}
Side effect annotations: Webpack assumes module initialization may have side effects (modifying globals, registering event listeners), preventing elimination even if exports are unused. Declare modules side-effect-free in package.json:
{
"name": "my-package",
"sideEffects": false // All files are side-effect-free
}
Or specify side-effect files explicitly:
{
"sideEffects": [
"*.css",
"./src/polyfills.js"
]
}
This enables Webpack to eliminate entire unused modules, not just unused exports.
Lodash tree shaking requires special handling:
// Bad: imports entire Lodash (71KB)
import _ from 'lodash';
_.debounce(fn, 300);
// Better: per-method imports
import debounce from 'lodash/debounce';
debounce(fn, 300);
// Best: lodash-es (ES6 module build)
import { debounce } from 'lodash-es';
debounce(fn, 300);
The lodash-es package provides ES6 module builds enabling tree shaking, unlike standard lodash which uses CommonJS.
Rollup tree shaking offers more aggressive elimination:
// rollup.config.js
import { terser } from 'rollup-plugin-terser';
export default {
input: 'src/index.js',
output: {
file: 'dist/bundle.js',
format: 'esm'
},
plugins: [terser()]
};
Rollup analyzes at statement level rather than module level, producing smaller bundles for library code compared to Webpack.
Dead code from conditional logic:
if (process.env.NODE_ENV !== 'production') {
console.log('Development mode');
enableDebugTools();
}
When NODE_ENV is defined as 'production' during build, bundlers eliminate this entire block as unreachable code. Ensure build processes define environment variables:
// webpack.config.js
const webpack = require('webpack');
module.exports = {
plugins: [
new webpack.DefinePlugin({
'process.env.NODE_ENV': JSON.stringify('production')
})
]
};
Analyzing bundle composition: Webpack Bundle Analyzer visualizes bundle contents:
npm install --save-dev webpack-bundle-analyzer
// webpack.config.js
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
module.exports = {
plugins: [
new BundleAnalyzerPlugin()
]
};
Running Webpack opens interactive treemap showing all modules and their sizes. Large modules consuming disproportionate space surface as optimization targets—consider lazy loading, splitting, or replacing with lighter alternatives.
Dynamic imports isolate unused features:
// Loads charting library only when user clicks "Show Chart"
button.addEventListener('click', async () => {
const { Chart } = await import('./charting.js');
new Chart(data);
});
Heavy features loaded conditionally don't bloat initial bundle. Coverage analysis shows 0% usage initially; the code only downloads when user action triggers import.
Code Splitting by Page and Feature
Code splitting divides JavaScript into route-specific or feature-specific bundles, eliminating cross-page code bloat where every page loads JavaScript for features that page doesn't use.
Route-based splitting isolates page-specific code:
// Without splitting - single bundle contains all pages
import HomePage from './pages/Home';
import ProductPage from './pages/Product';
import CheckoutPage from './pages/Checkout';
// With splitting - separate bundles per route
const routes = [
{ path: '/', component: () => import('./pages/Home') },
{ path: '/product', component: () => import('./pages/Product') },
{ path: '/checkout', component: () => import('./pages/Checkout') }
];
React Router + lazy loading:
import { lazy, Suspense } from 'react';
const Home = lazy(() => import('./pages/Home'));
const Product = lazy(() => import('./pages/Product'));
function App() {
return (
<Suspense fallback={<div>Loading...</div>}>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/product" element={<Product />} />
</Routes>
</Suspense>
);
}
Each route loads only its own bundle, eliminating homepage code from product pages and vice versa.
Feature-based splitting:
// Heavy feature loaded conditionally
const videoPlayer = () => import('./features/VideoPlayer');
document.querySelector('.video-trigger').addEventListener('click', async () => {
const { VideoPlayer } = await videoPlayer();
new VideoPlayer(config);
});
Video player code (potentially hundreds of KB with codec support) only loads when users click video triggers.
Vendor bundle splitting:
// webpack.config.js
module.exports = {
optimization: {
splitChunks: {
cacheGroups: {
vendor: {
test: /[\\/]node_modules[\\/]/,
name: 'vendor',
chunks: 'all'
}
}
}
}
};
This extracts all third-party dependencies into vendor.js, separating stable library code from frequently changing application code. When you update app code, users reuse cached vendor bundle.
Granular vendor splitting:
splitChunks: {
cacheGroups: {
react: {
test: /[\\/]node_modules[\\/](react|react-dom)[\\/]/,
name: 'react',
priority: 20
},
lodash: {
test: /[\\/]node_modules[\\/]lodash[\\/]/,
name: 'lodash',
priority: 20
},
vendor: {
test: /[\\/]node_modules[\\/]/,
name: 'vendor',
priority: 10
}
}
}
Separate bundles for React, Lodash, and other vendors optimize cache granularity—updating React doesn't invalidate Lodash cache.
Critical vs non-critical splitting:
// Critical: Load immediately
import { formatCurrency } from './utils';
// Non-critical: Lazy load
const analytics = () => import('./analytics');
window.addEventListener('load', async () => {
const { track } = await analytics();
track('page-view');
});
Analytics, chat widgets, A/B testing—defer until load event or user interaction, keeping initial bundles minimal.
Preloading split chunks:
<link rel="preload" href="/product-page.js" as="script">
Preloading anticipated routes (product pages when user hovers product links) eliminates load delay when navigation occurs.
WordPress and CMS-Specific Optimization
WordPress unused CSS/JS accumulates from themes and plugins loading assets globally when they're needed only on specific pages.
Asset CleanUp plugin (free) provides granular control:
- Install Asset CleanUp
- Browse site logged in
- Plugin adds control panel to each page showing all loaded CSS/JS
- Unload unused assets per page type
- Configure global rules (e.g., "Never load Contact Form 7 CSS except on /contact page")
Perfmatters plugin (premium, $29/year):
- Install Perfmatters
- Navigate to Settings → Assets
- Configure script/style loading per page type
- Enable "Remove jQuery Migrate" if unused
- Defer JavaScript, disable emojis, remove query strings
Manual theme optimization:
// functions.php - conditional asset loading
function load_conditional_assets() {
// Remove unused assets
wp_dequeue_style('wp-block-library'); // Gutenberg styles if not using blocks
// Load Contact Form 7 only on contact page
if (!is_page('contact')) {
wp_dequeue_style('contact-form-7');
wp_dequeue_script('contact-form-7');
}
// Load WooCommerce assets only on shop pages
if (!is_woocommerce()) {
wp_dequeue_style('woocommerce-layout');
wp_dequeue_style('woocommerce-general');
wp_dequeue_script('woocommerce');
}
}
add_action('wp_enqueue_scripts', 'load_conditional_assets', 999);
This dequeues specific stylesheets and scripts based on page context, preventing global loading.
Shopify theme optimization:
Shopify themes load assets via {{ 'theme.css' | asset_url | stylesheet_tag }}. Unused code removal requires editing theme files:
- Download theme files
- Run PurgeCSS against Liquid templates + theme.css
- Replace theme.css with purged version
- Upload modified theme
- Test across product, collection, cart, checkout pages
Elementor/page builder cleanup: Page builders inject large CSS/JS libraries. Optimize via plugins or code:
// Remove Elementor frontend CSS/JS on non-Elementor pages
function remove_elementor_assets() {
if (!is_page_elementor()) {
wp_dequeue_style('elementor-frontend');
wp_dequeue_script('elementor-frontend');
}
}
function is_page_elementor() {
$post_id = get_the_ID();
return \Elementor\Plugin::$instance->db->is_built_with_elementor($post_id);
}
add_action('wp_enqueue_scripts', 'remove_elementor_assets', 999);
Monitoring and Preventing Unused Code Accumulation
Automated coverage reports in CI/CD pipelines catch unused code before production:
// coverage-check.js
const puppeteer = require('puppeteer');
(async () => {
const browser = await puppeteer.launch();
const page = await browser.newPage();
await page.coverage.startCSSCoverage();
await page.coverage.startJSCoverage();
await page.goto('https://example.com');
const [cssCoverage, jsCoverage] = await Promise.all([
page.coverage.stopCSSCoverage(),
page.coverage.stopJSCoverage()
]);
const cssUnused = cssCoverage.reduce((total, entry) => {
return total + entry.ranges.reduce((sum, range) => {
return sum + (range.end - range.start);
}, 0);
}, 0);
const cssTotal = cssCoverage.reduce((total, entry) => total + entry.text.length, 0);
const cssUsagePercent = ((cssTotal - cssUnused) / cssTotal * 100).toFixed(2);
console.log(`CSS Usage: ${cssUsagePercent}%`);
if (cssUsagePercent < 50) {
console.error('ERROR: CSS usage below 50% threshold');
process.exit(1); // Fail CI build
}
await browser.close();
})();
Run this script in CI pipeline to fail builds when unused code exceeds thresholds, preventing regressions.
Bundle size limits enforce discipline:
// webpack.config.js
module.exports = {
performance: {
maxAssetSize: 250000, // 250KB
maxEntrypointSize: 400000, // 400KB
hints: 'error' // Fail build if exceeded
}
};
Webpack fails builds when bundle sizes exceed limits, forcing developers to optimize before deployment.
Bundlephobia integration checks dependency sizes before installation:
npx bundle-phobia lodash
# Output: lodash@4.17.21 - minified: 71.5KB, gzipped: 25.0KB
Review package sizes before adding dependencies, preferring lighter alternatives when functionality overlaps.
Regular audits: Schedule quarterly unused code audits:
- Run Coverage analysis on production site
- Document files with 60%+ unused code
- Create tickets for cleanup
- Prioritize by file size × unused percentage
- Implement PurgeCSS/tree shaking or manual removal
- Measure before/after bundle size reduction
Documentation requirements: Require developers document why large dependencies are added:
// package.json
{
"dependencies": {
"moment": "^2.29.0" // Required for timezone handling in booking system
}
}
Comments prevent "why do we have this?" confusion later, enabling informed decisions about continued use or replacement.
FAQ: Unused CSS and JavaScript Removal
Will removing unused CSS break responsive designs? No if PurgeCSS configuration includes all content files (HTML, JS, templates). Responsive classes in media queries are preserved when found in content. Test across all breakpoints after purging to verify responsive behavior.
Can I remove jQuery if plugins depend on it? No. If any WordPress plugins or theme features require jQuery, removing it breaks functionality. Instead, defer jQuery loading (load at body end) and audit plugins—many modern alternatives eliminate jQuery dependence entirely.
Does tree shaking work with TypeScript?
Yes. Configure TypeScript to preserve ES6 modules ("module": "esnext" in tsconfig.json), then Webpack tree shakes TypeScript-compiled output normally. TypeScript's type information doesn't interfere with tree shaking.
How do I handle CSS frameworks like Bootstrap after purging? PurgeCSS reduces Bootstrap from 150KB to 15-30KB typically, removing unused utilities and components. Test thoroughly—some JavaScript components (modals, dropdowns) require specific CSS classes that may be purged if not found in templates.
Can removing unused code hurt SEO? No. Search engines don't penalize small file sizes. Reduced CSS/JS improves page speed metrics (FCP, TBT), which positively impact rankings through Core Web Vitals. Ensure purging doesn't remove styles affecting Googlebot's rendered view.
Should I remove unused CSS from third-party widgets? Risky. Third-party widgets (chat, ads, social) may inject HTML dynamically after page load, requiring CSS not detected in initial content. Safelist third-party CSS selectors or exclude third-party CSS from purging.
Does code splitting hurt SEO by delaying content? No if critical content renders immediately. Lazy-loaded features (modals, videos, advanced UI) don't affect SEO. Ensure primary text content and navigation render without waiting for split bundles.
How do I test for broken styles after PurgeCSS? Visual regression testing across all templates and breakpoints. Automated tools (Percy, BackstopJS) capture screenshots before/after purging, highlighting visual differences. Manual QA should test interactive states (hover, focus, active) and JavaScript-triggered UI changes.
When This Fix Isn't Your Priority
Skip this for now if:
- Your site has fundamental crawling/indexing issues. Fixing a meta description is pointless if Google can't reach the page. Resolve access, robots.txt, and crawl errors before optimizing on-page elements.
- You're mid-migration. During platform or domain migrations, freeze non-critical changes. The migration itself introduces enough variables — layer optimizations after the new environment stabilizes.
- The page gets zero impressions in Search Console. If Google shows no data for the page, the issue is likely discoverability or indexation, not on-page optimization. Investigate why the page isn't indexed first.
Frequently Asked Questions
How long does this fix take to implement?
Most fixes in this article can be implemented in under an hour. Some require a staging environment for testing before deploying to production. The article flags which changes are safe to deploy immediately versus which need QA review first.
Will this fix work on WordPress, Shopify, and custom sites?
The underlying SEO principles are platform-agnostic. Implementation details differ — WordPress uses plugins and theme files, Shopify uses Liquid templates, custom sites use direct code changes. The article focuses on the what and why; platform-specific how-to links are provided where available.
How do I verify the fix actually worked?
Each fix includes a verification step. For most technical SEO changes: check Google Search Console coverage report 48-72 hours after deployment, validate with a live URL inspection, and monitor the affected pages in your crawl tool. Ranking impact typically surfaces within 1-4 weeks depending on crawl frequency.