How to Reduce JavaScript and Speed Up Your Site
Excessive JavaScript is costly to download and run, hurting INP and LCP. How to reduce it: code-splitting, tree-shaking, defer/async and auditing third parties.
JavaScript is the most expensive resource on the modern web. Not because of its file size alone, but because every kilobyte has to be downloaded, then parsed, compiled and executed on the main thread — the same thread that responds to taps and paints the screen. In short: excessive JavaScript blocks the main thread, which is the primary cause of poor INP and a frequent drag on LCP; the fix is to ship less of it through code-splitting, tree-shaking, deferring, removing dead code and auditing third parties. This guide explains why JavaScript is so costly, the tactics that reduce it, and exactly how to find the heaviest offenders on your site.
It is the practical companion to INP (Interaction to Next Paint) explained, the metric most affected by JavaScript.
Why JavaScript hurts so much
Unlike an image, which the browser simply downloads and displays, JavaScript imposes a second, hidden cost: execution. The browser must parse the code, compile it, and run it — and it does this on the main thread, the single thread responsible for handling user input, running scripts and painting the page.
When the main thread is busy executing JavaScript, two things suffer:
- Responsiveness. Taps, clicks and key presses queue up unanswered while a script runs, which is exactly what Interaction to Next Paint (INP) measures. Any task that runs for 50ms or more is a long task that blocks input.
- Loading. During page load, downloading and executing scripts competes with images, CSS and fonts for the network and the main thread, delaying when the largest element renders — hurting Largest Contentful Paint.
So JavaScript is uniquely worth optimising: reducing it improves both responsiveness and loading at the same time, and it is the most common root cause of field-data failures.
The tactics, by impact
| Tactic | What it does | Impact |
|---|---|---|
| Remove unused JavaScript | Delete dead code and unused libraries | High — pure waste eliminated |
| Audit third-party scripts | Cut or defer analytics, chat, A/B, tags | High — often the biggest blockers |
| Code-splitting | Load only the JS each page needs | High — smaller initial bundles |
| Defer / async | Stop scripts blocking HTML parsing/render | Medium-high — faster first render |
| Tree-shaking | Drop unused exports at build time | Medium — leaner bundles |
| Lazy-load non-critical JS | Load on interaction or when needed | Medium — less upfront work |
| Minification | Strip whitespace, shorten names | Low-medium — smaller downloads |
The pattern is clear: the biggest wins come from shipping less code in the first place — removing what is unused and questioning third parties — rather than from squeezing the code you keep.
Remove unused JavaScript
The cheapest byte is the one you never ship. Over time, sites accumulate dead code, abandoned features and libraries pulled in for a single function. Audit and delete what is no longer used, replace heavyweight libraries with lighter alternatives or native browser APIs (modern JavaScript does natively much of what big utility libraries once provided), and remove polyfills for browsers you no longer support. Lighthouse's "Reduce unused JavaScript" audit and the DevTools Coverage tab (below) make the waste visible.
Audit third-party scripts ruthlessly
Third-party scripts — analytics, chat widgets, A/B testing, ad and marketing tags, social embeds, tag managers — are frequently the single biggest source of main-thread blocking, and they are easy to add and easy to forget. Each one downloads and executes code you do not control, often loading further scripts of its own.
Treat every third party as a cost to justify:
- Inventory what is loaded (the DevTools Network panel and the "Reduce the impact of third-party code" audit help).
- Remove anything not actively earning its place.
- Defer the rest so it loads after the critical content, not during it.
- Consolidate tags through a single, well-governed manager rather than scattering snippets.
- Be wary of tag managers that let non-engineers add scripts freely — they are a common way for a fast site to slowly become slow.
This audit alone often recovers more performance than any amount of bundle tuning, because third parties can dwarf your own code on the main thread.
Code-splitting and lazy-loading
Shipping your entire application's JavaScript on the first page load is wasteful — most visitors never touch most of it on any given page. Code-splitting breaks the bundle into smaller chunks loaded on demand: the checkout code loads when someone reaches checkout, not when they view the homepage. Lazy-loading extends this to components and scripts that are not needed immediately — a heavy chart, a map, a modal — loading them on interaction or when they scroll into view. Together they slash the JavaScript that runs during the critical initial load, which is when it does the most damage.
Defer and async
By default, a <script> in the <head> blocks HTML parsing while it downloads and runs, delaying first render. Two attributes fix this:
defer— downloads the script without blocking parsing and runs it after the HTML is parsed, in document order. This is the safe default for most scripts: non-blocking and order-preserving.async— downloads without blocking and runs as soon as it is ready, which can interrupt parsing and does not preserve order. Use it only for independent scripts where order does not matter (some analytics).
Moving non-critical scripts to defer is a quick, low-risk win that lets the page render before the JavaScript has finished.
Tree-shaking and minification
At build time, two automated steps reduce what you ship:
- Tree-shaking removes unused exports — code you imported but never actually call — from your bundles. Modern bundlers do this when your code uses ES modules and avoids patterns that defeat static analysis.
- Minification strips whitespace, comments and shortens variable names, reducing file size with no behavioural change. Combined with compression (Brotli or gzip) at the server or CDN, it meaningfully cuts download size.
These are largely set-and-forget in a modern build pipeline, but worth verifying are actually enabled in production.
Mind hydration in single-page apps
Frameworks that server-render and then hydrate (attach interactivity to server-rendered HTML in the browser) can create a burst of main-thread work right when the user first tries to interact — a classic INP problem. If you use such a framework, look into partial or progressive hydration, islands architecture, and streaming, which hydrate less and later. The general rule holds: ship less JavaScript, hydrate less, and do it later where you can. And remember the framework is rarely the whole problem — it is usually the accumulation of features and third parties layered on top that fills the main thread.
How to find heavy JavaScript
A repeatable diagnosis process:
- Lighthouse / PageSpeed Insights — run the audit and read "Reduce unused JavaScript", "Reduce the impact of third-party code" and "Minify JavaScript", each with estimated savings.
- DevTools Coverage tab — open it, reload the page, and see what percentage of each script and stylesheet went unused, line by line. High unused percentages point straight at code-splitting or removal opportunities.
- DevTools Performance panel — record a load and an interaction; the main-thread track shows long tasks (flagged with red corners) and which scripts caused them, so you can target the worst.
- DevTools Network panel — sort by size to see the largest scripts and identify third-party origins.
Attack the largest and most blocking offenders first; performance work has steep diminishing returns, so the top two or three items usually deliver most of the gain.
One caveat worth internalising: the execution cost of JavaScript scales with the speed of the user's device, not yours. A bundle that parses and runs in 100ms on a fast developer laptop can take several times longer on a mid-range phone, because parsing and execution are CPU-bound. This is why a site can feel instant on the machine it was built on yet post poor INP in the field, where a large share of real visitors are on modest hardware. When you test, throttle the CPU in the DevTools Performance panel (a 4x or 6x slowdown approximates a typical phone) so the numbers you see resemble what your slower users actually experience. Field data from the Chrome UX Report remains the ultimate arbiter, precisely because it averages across the full range of devices rather than flattering you with your own.
A worked example
Suppose Lighthouse flags 350KB of unused JavaScript and your INP is "needs improvement". The Coverage tab shows a large utility library is 80% unused and a charting library loads on every page but is only used on one. You make three changes: replace the utility library's handful of used functions with native JavaScript, code-split the charting library so it loads only on the page that needs it, and defer two third-party tags that were running during load. The initial bundle shrinks substantially, long tasks during load shorten, and field INP and LCP both improve. This is the typical shape of JavaScript optimisation — a few large, identifiable offenders rather than a thousand small ones — which is why measuring first is what makes the work efficient.
The sustainability bonus
Less JavaScript is not only faster, it is greener. Code that is never downloaded is never transmitted, and code that is never executed never burns CPU cycles on the user's device — saving energy at both ends. Reducing JavaScript therefore lowers a page's carbon footprint at the same time as improving its speed, another instance of performance and sustainability pulling in the same direction.
Common mistakes
- Shipping the whole app upfront instead of code-splitting per route.
- Letting third parties accumulate unchecked via a permissive tag manager.
- Using
asyncwheredeferbelongs, causing order and parsing issues. - Optimising your own code while ignoring third parties that dwarf it.
- Measuring only in the lab, when INP depends on real interaction patterns.
Go deeper
- The metric JavaScript hurts most: INP (Interaction to Next Paint) explained.
- The big picture, in priority order: how to make your website load faster.
- The full set of metrics: Core Web Vitals explained.
- The efficiency angle: what is a website carbon footprint and how to reduce it.
Want to see how much JavaScript your site ships, alongside performance, SEO and security? Analyse any URL with StackOptic — free, no sign-up.
Frequently asked questions
Why is too much JavaScript bad for performance?
JavaScript imposes two costs. First, it must be downloaded, adding to page weight and competing for bandwidth with other resources. Second, and often more damaging, the browser must parse, compile and execute it on the main thread — the same thread that handles user input and rendering. While the main thread is busy running scripts, the page cannot respond to taps and clicks or paint updates, so it feels slow and unresponsive.
How does JavaScript affect Core Web Vitals?
It hits two of the three metrics. Excess JavaScript is the main cause of poor Interaction to Next Paint (INP), because long tasks block the main thread from responding to user input. It also hurts Largest Contentful Paint (LCP) by competing for the network during load and by delaying rendering when content depends on scripts running first. Reducing JavaScript therefore improves both responsiveness and loading at once.
What is code-splitting?
Code-splitting breaks a single large JavaScript bundle into smaller chunks that load only when needed, instead of shipping all of your code on the first page load. For example, the code for a checkout page does not need to download when someone views the homepage. Combined with lazy-loading, it means each page sends only the JavaScript it actually requires, which cuts download and execution time on the initial load.
What is the difference between defer and async?
Both let an external script download without blocking HTML parsing, but they differ in execution. A script with async runs as soon as it finishes downloading, which can interrupt parsing and does not guarantee order. A script with defer waits until HTML parsing is complete and runs scripts in document order. For most non-critical scripts, defer is the safer default because it preserves order and never blocks the initial render.
How do I find unused JavaScript on my site?
Run Lighthouse (in Chrome DevTools or PageSpeed Insights) and look at the 'Reduce unused JavaScript' and 'Reduce unused CSS' audits, which estimate wasted bytes. For a live, line-level view, open the Coverage tab in Chrome DevTools, reload the page, and it shows what percentage of each file went unused. The Performance panel reveals which scripts cause long main-thread tasks, so you can prioritise the worst offenders.
Analyse any website with StackOptic
Get the full technology stack, performance, security and SEO report in seconds — free.
Analyse a websiteRelated articles
How to Test Your Website Speed (Tools and Metrics)
Which tools to use, lab versus field data, and the metrics that matter. Test your site speed with PageSpeed Insights, Lighthouse, WebPageTest and DevTools.
How to Optimize CSS for Faster Pages
CSS is render-blocking, so bloated stylesheets delay your page. How to minify, cut unused CSS, inline critical CSS and defer the rest to speed up FCP and LCP.
What Are Gzip and Brotli Compression (and How to Enable Them)?
Gzip and Brotli shrink HTML, CSS and JavaScript before they are sent. What each is, how Brotli compares to Gzip, how to verify it, and how to enable it.