Why unused JavaScript damages your Core Web Vitals
The average web page ships 500 KB of JavaScript to the browser. This figure, published by HTTP Archive in its 2025 Web Almanac, is the manifestation of a systemic problem: the web industry has normalised sending large amounts of code that the majority of users will never execute. JavaScript accumulates sprint after sprint, dependency after dependency, plugin after plugin, until the total weight becomes an invisible drag on performance.
The difference between CSS and JavaScript in terms of performance impact is that CSS blocks rendering but processes quickly, while JavaScript must be downloaded, parsed, compiled and executed — a significantly more expensive computational pipeline. According to Google’s V8 team, each additional 100 KB of JavaScript adds 150 to 350 ms of processing time on a mid-range mobile device (a Samsung Galaxy A54 or Xiaomi Redmi Note — among the most popular devices globally). On high-end devices like an iPhone 15 Pro, the same JavaScript takes 50-100 ms. The 4x to 7x difference between high-end and mid-range devices explains why a website can feel fast on a developer’s MacBook and painfully slow for 60% of its actual users.
The impact on Core Web Vitals is direct and measurable. LCP is delayed when JavaScript blocks the main thread during initial load, preventing the browser from painting the largest content element. INP (Interaction to Next Paint) degrades when long-running scripts keep the main thread busy, preventing the browser from responding to user interactions. CLS increases when JavaScript modifies the DOM after initial render, shifting visible elements.
Chrome DevTools Coverage reveals the scale of waste. In our audits, 40% to 70% of downloaded JavaScript is never executed on the current page. That dead code consumes bandwidth, parse time and CPU without contributing any functionality. It is the digital equivalent of paying for a warehouse full of inventory that never sells — a fixed cost that drags margins without generating value.
The relationship with web speed and SEO is clear. Google measures Core Web Vitals with field data — real Chrome users — and uses those metrics as ranking signals. A site with 500 KB of unnecessary JavaScript can have an INP of 400 ms on mid-range devices, well above the 200 ms threshold Google considers “good”. Removing that unused JavaScript can be the difference between passing or failing the Core Web Vitals thresholds that directly impact organic rankings.
How to identify unused JavaScript with Chrome DevTools
Accurate identification of unused JavaScript is the essential prerequisite for any optimization. Without a clear diagnosis of what code is not executing, interventions are speculative and the risk of breaking functionality is high.
Chrome DevTools Coverage
Access it from DevTools (F12) > Ctrl+Shift+P > “Show Coverage” > reload button. Coverage analyses the execution of each JavaScript and CSS file during a user session, marking in red the lines that did not execute and in blue those that did. The result is a utilisation percentage per file: a file with 30% utilisation means 70% of its code is potentially removable.
The correct methodology is: first, perform a clean page reload to capture initial CSS and JS; second, interact with all page elements — menus, modals, tabs, forms — to activate code that runs on demand; third, navigate to 3-5 representative pages across the site. The final result shows real JavaScript utilisation at the site level, not just for an individual page. A file with 15% utilisation after this full methodology is a clear candidate for optimization.
Lighthouse
Lighthouse complements Coverage with its “Remove unused JavaScript” audit. While Coverage shows granular data per file and line, Lighthouse quantifies the potential savings in KB and seconds. The audit lists the files with the greatest amount of unused code and estimates the impact on loading time. For prioritisation, files that Lighthouse flags with more than 50 KB of potential savings deserve immediate attention.
Webpack Bundle Analyzer
Webpack Bundle Analyzer (or its Vite equivalent: rollup-plugin-visualizer) provides a treemap view of the complete JavaScript bundle, showing which npm dependencies occupy the most space. It is common to discover that a single dependency — moment.js (300 KB), the full lodash package (70 KB), or an icon library imported in its entirety — accounts for 30-50% of the total bundle. These dependencies are the priority candidates for tree shaking, replacement or removal.
A pattern we frequently detect in e-commerce sites is the accumulation of tracking scripts. Google Tag Manager, Facebook Pixel, Hotjar, Clarity, TikTok Pixel, Pinterest Tag, Google Ads remarketing, and affiliate platform scripts can easily add 300-500 KB of additional JavaScript. Each of these scripts runs on the main thread, competing with the application’s own JavaScript for CPU time. Auditing third-party scripts is as important as auditing first-party code.
Third-party JavaScript: the worst offender (and how to manage it)
Third-party JavaScript is the most frequent source of unused JavaScript and, paradoxically, the hardest to control. Unlike first-party code, where the development team can modify, optimize or remove, third-party code is a black box loaded from external servers executing logic over which there is no direct control.
According to HTTP Archive’s Web Almanac, the median website loads scripts from 8 different third-party origins. Each origin introduces DNS resolution latency, TCP/TLS connection establishment and data transfer. But the real cost is not the download — it is the execution. A live chat widget weighing 150 KB after download can expand to 400 KB of executable JavaScript, monopolising the main thread for 500-800 ms on mid-range devices.
Strategies for managing third-party JavaScript, ordered by impact, are as follows.
Audit and remove the unnecessary
The first step is to inventory all third-party scripts loaded on the site and question whether each one delivers value proportional to its performance cost. A survey widget that generated 12 responses in the past 6 months does not justify 80 KB of JavaScript on every page load. An A/B testing script that is not actively in use does not justify 120 KB of SDK. Pure removal is the most effective optimization: zero bytes is always faster than any optimized amount.
Facade Pattern for on-demand loading
The Facade Pattern shows a lightweight visual placeholder (a static image or a CSS-only component) instead of the real widget, loading the full JavaScript only when the user interacts with it. The most documented example is the YouTube embed: replacing the YouTube iframe with a thumbnail image and a play button, then loading the full player on click, saves 500-700 KB of JavaScript on initial load. The same pattern applies to chatbots, Google Maps, and social media widgets.
Defer and async for non-critical scripts
The defer attribute on a <script> tag tells the browser to download the script in parallel with HTML parsing but execute it only after the DOM is complete. The async attribute downloads in parallel but executes immediately when the download finishes, potentially interrupting parsing. For analytics and tracking scripts, defer is the correct choice. For scripts that need to execute as early as possible without depending on the DOM (such as content preloads), async is appropriate.
Google Tag Manager with activation rules
GTM allows control over which scripts load on which pages through activation rules (triggers). A Facebook pixel configured to fire only on product and checkout pages, rather than on every page, reduces JavaScript load on informational pages where that pixel does not generate conversions. This granularity requires initial configuration, but the performance benefit is proportional to the number of scripts managed.
Tree shaking and code splitting: removing dead code at build time
Tree shaking and code splitting are complementary techniques that operate within the build pipeline to reduce the JavaScript that reaches the browser. Tree shaking removes code that exists but is never called. Code splitting divides code into chunks that load on demand.
Tree shaking
Tree shaking is an automatic process in modern bundlers (Webpack 5, Vite/Rollup, esbuild) that analyses ES module imports (import/export) and identifies exports that no other module consumes. Those dead exports are removed from the final bundle. The name “tree shaking” comes from the metaphor of shaking a tree to let dead leaves fall.
The fundamental requirement for tree shaking to work is using ES modules (ESM) with import and export, not CommonJS with require and module.exports. CommonJS is dynamic — imports can depend on runtime conditions — and bundlers cannot determine statically what is used. ESM is static: imports are declared at the top of the file and the bundler can analyse them without executing the code.
A textbook example is the difference between import _ from 'lodash' (imports the entire library, 70 KB) and import { debounce } from 'lodash-es' (imports only the needed function, 3 KB). Using the ESM variant (lodash-es) allows the bundler to apply tree shaking and eliminate the 300+ lodash functions that are not used. For dependencies that do not offer an ESM variant, alternatives like babel-plugin-lodash or babel-plugin-import transform full imports into selective imports during the build.
Code splitting
Code splitting divides the JavaScript bundle into multiple chunks that load on demand. The primary technique is the dynamic import(): instead of import { Gallery } from './Gallery' (static load, included in the main bundle), use const { Gallery } = await import('./Gallery') (dynamic load, generates a separate chunk downloaded only when needed).
Modern frameworks implement code splitting by default at the route level. Next.js generates a separate chunk for each page. Astro goes further with its Islands architecture, where each interactive component loads as an independent chunk only when it becomes visible in the viewport (client:visible). This granularity transforms code splitting from a manual optimization into the framework’s default behaviour.
The combination of tree shaking + code splitting + lazy component loading can reduce the initial JavaScript payload by 50-70% in medium-sized applications. For a SPA with 1.2 MB total JavaScript, this means going from shipping 1.2 MB on initial load to shipping 350-500 KB, with the rest loaded progressively as the user navigates.
Defer vs async: when to use each script attribute
The defer and async attributes control when and how the browser downloads and executes external scripts. Using them correctly can improve FCP and INP without modifying the JavaScript code itself. Using them incorrectly can break page functionality.
Without defer or async (normal script)
The browser pauses HTML parsing when it encounters the <script>, downloads the file, executes it, and only then resumes parsing. This is render-blocking and is the primary reason scripts in the <head> delay FCP. It should only be used for scripts that must execute before the user sees any content — extremely rare cases such as feature detection or critical polyfills.
With async
The browser downloads the script in parallel with HTML parsing but executes it immediately when the download finishes, pausing parsing if necessary. Execution order among multiple async scripts is not guaranteed: the first to finish downloading executes first. This means async is unsuitable for scripts that depend on other scripts. It is appropriate for fully independent scripts like analytics (Google Analytics, Plausible) or self-contained third-party widgets.
With defer
The browser downloads the script in parallel with HTML parsing and executes it only after the DOM is fully constructed, just before the DOMContentLoaded event. Execution order among multiple deferred scripts is maintained: they execute in the order they appear in the HTML. Defer is the correct option for most application scripts that need DOM access.
The practical recommendation is: defer for all application scripts that access the DOM, async for independent third-party scripts (analytics, pixels), and neither defer nor async only for critical polyfills that must be available before any other script executes.
A common mistake is placing deferred scripts at the end of the <body> thinking this makes them load later. With defer in the <head>, the script downloads in parallel with HTML parsing from the very beginning, but executes at the end. In the <body> without defer, the script downloads and executes sequentially when the parser reaches that position. The first case is faster because the download starts earlier.
The performance difference is measurable. In a WebPageTest simulation on 4G, moving three third-party scripts (GTM, Facebook Pixel, Hotjar) from normal loading to defer improved page INP from 380 ms to 180 ms, crossing Google’s “good” threshold of 200 ms. FCP also improved by 0.4 seconds because the scripts stopped blocking the main thread during initial load.
Metrics: how much you can improve by removing unused JS
Quantifying potential impact before implementing changes enables intervention prioritisation and justifies the investment of development time. The key metrics for evaluating unused JavaScript are: total JS weight, percentage of unexecuted code, main thread processing time, and the affected Core Web Vitals metrics.
Total JavaScript weight
Measured by filtering by type “Script” in the DevTools Network tab. Transferred weight (compressed) is what impacts download time. Uncompressed weight (parsed) is what impacts parse and compilation time. For the median web page, transferred weight is 500 KB and parsed weight is 1.5-2 MB (the gzip/brotli compression ratio for JavaScript is typically 3:1 to 4:1).
Percentage of unexecuted code
Provided by Chrome DevTools Coverage. The benchmark is: below 30% unused code is acceptable, 30-50% requires attention, above 50% is a critical problem. The median website has 45% unused JavaScript, according to Coverage data collected by Web Almanac.
Main thread processing time
Visualised in the DevTools Performance tab. The “Scripting” category shows how many milliseconds of loading time are spent compiling and executing JavaScript. For pages with LCP above 2.5 seconds, if Scripting exceeds 500 ms, reducing JavaScript is the priority intervention.
Documented results from removing unused JavaScript are consistent. A web.dev case study on a news site showed that reducing JavaScript from 800 KB to 350 KB improved LCP from 3.8 to 2.1 seconds on mid-range mobile devices. INP improved from 350 ms to 120 ms. Mobile organic traffic grew 15% in the eight weeks following implementation.
For e-commerce sites with JavaScript SEO issues, reducing JavaScript provides a dual benefit: it improves Core Web Vitals for real users and reduces the computational cost for Googlebot, which must execute JavaScript to index pages using Client-Side Rendering. Less JavaScript means faster and more complete indexation.
The prioritisation rule we recommend is: first, remove non-essential third-party scripts (highest impact with lowest risk); second, implement defer/async on remaining scripts (medium impact, low risk); third, apply route-level code splitting (high impact, medium risk); and fourth, optimize tree shaking of npm dependencies (variable impact, low risk). This sequence maximises return per unit of effort and minimises the probability of functional regressions.