JavaScript Performance: When Less Code Means a Better User Experience
There’s a paradox in modern web development. We have more powerful tools and frameworks than ever, yet the average web page is slower and heavier than it was five years ago. The primary culprit? JavaScript.
The average webpage now ships over 500KB of JavaScript. Many sites (including small business websites that have no business being this heavy) push well over a megabyte. d unlike images, which can be lazily loaded and progressively rendered, JavaScript must be downloaded, parsed, compiled, and executed before it does anything useful. Every kilobyte of JavaScript costs more in performance than a kilobyte of almost anything else. We go deeper on all of this in a plain English guide to website performance and Core Web Vitals.
We’ve audited small business websites running WordPress with a half-dozen plugins that ship 1.2MB of JavaScript to render what is fundamentally a brochure site. A site with five pages, a contact form, and a photo gallery does not need a megabyte of scripts. It needs maybe 30KB.
This post is about understanding where JavaScript bloat comes from, how to measure it, and practical strategies for shipping less of it, resulting in faster, more responsive websites that rank better and convert more visitors.
Why JavaScript Is Uniquely Expensive
Not all bytes are created equal. A 200KB image and 200KB of JavaScript are not the same thing from a performance perspective.
When the browser downloads an image, it can progressively render it, showing a low-quality version first and improving as more data arrives. The main thread isn’t blocked. The user can interact with the page while images load.
JavaScript is different. The browser must:
- Download the script file (same as any asset)
- Parse the code, converting text to an abstract syntax tree
- Compile the code, converting it to executable bytecode
- Execute the code: running it, which may modify the DOM, make network requests, and attach event listeners
Steps 2-4 happen on the main thread, which means nothing else can happen while they’re running. No scrolling, no clicking, no animations. The page is frozen.
On a fast desktop machine, parsing and compiling 200KB of JavaScript takes maybe 50-100ms. On a mid-range Android phone (the kind most people actually use), it takes 300-500ms. n a budget phone on a slow connection, it can take well over a second.
Multiply that by several scripts, and you’re looking at multiple seconds of main thread blocking. The page might look loaded, but it doesn’t respond to taps or clicks. Users think the site is broken.
This is what Google measures with the Interaction to Next Paint (INP) metric. If your JavaScript is hogging the main thread, your INP score suffers, and so does your search ranking.
Where the Bloat Comes From
Understanding the sources of JavaScript bloat is the first step to reducing it.
Heavy Frameworks for Simple Needs
React, Vue, Angular: these are powerful frameworks designed for complex, interactive web applications. A dashboard with real-time data, drag-and-drop interfaces, and complex state management genuinely benefits from a framework.
A brochure website with a navigation menu, a contact form, and a photo slider does not.
Yet we see this constantly. A developer reaches for React because it’s what they know, and suddenly a five-page business website ships 150KB of framework code before a single line of custom functionality is added.
Framework sizes (minified + gzipped):
For many small business websites, the right framework is no framework at all.
Plugin and Package Bloat
Every npm package you install adds to your bundle. And each package has its own dependencies, which have their own dependencies. It compounds quickly.
Common offenders:
date-fns (tree-shakeable, 2-4KB for common operations) or the native Intl.DateTimeFormat API (0KB).import debounce from 'lodash/debounce' instead of import _ from 'lodash'.document.querySelector replaced $(). fetch replaced $.ajax. classList replaced $.addClass.WordPress Plugin JavaScript
WordPress sites are particularly vulnerable to JavaScript bloat because every plugin can enqueue its own scripts. We regularly see:
- Contact form plugins loading scripts on every page, not just the contact page
- Slider plugins loading on pages without sliders
- Social sharing plugins adding 100KB+ of scripts
- Analytics and tracking scripts multiplying across plugins
- “Optimization” plugins that paradoxically add more JavaScript than they save
Third-Party Scripts
Analytics, chat widgets, marketing tags, A/B testing tools, ad networks. Each adds JavaScript that you don’t control and often can’t optimize:
These scripts also make network requests to external servers, adding latency beyond just the script weight.

Measuring Your JavaScript Footprint
Before optimizing, measure what you’re working with.
Chrome DevTools Coverage Tab
The Coverage tab shows you exactly how much of your downloaded JavaScript is actually being executed on the current page. Open DevTools, go to the Coverage tab (Ctrl+Shift+P, type “Coverage”), reload the page, and look at the results.
You’ll likely see that 40-70% of your JavaScript is unused on any given page. That’s dead code being shipped to every visitor.
Bundle Analyzer Tools
For projects using webpack or similar bundlers:
- webpack-bundle-analyzer: creates a visual treemap of your bundle, showing exactly which packages consume the most space
- source-map-explorer: similar visualization using source maps
- bundlephobia.com: check the size of any npm package before installing it
Lighthouse Performance Audit
Lighthouse flags JavaScript issues including:
- Unused JavaScript (with specific file-by-file breakdown)
- Main thread blocking time
- JavaScript execution time
- Opportunities to reduce JavaScript payload
Web Vitals Measurement
Track INP and Total Blocking Time (TBT) as your primary JavaScript performance indicators. If these metrics are good, your JavaScript isn’t causing problems. If they’re bad, JavaScript is likely the culprit.
Strategies for Shipping Less JavaScript
Tree Shaking
Tree shaking is the process of eliminating dead code from your JavaScript bundle. Modern bundlers (webpack, Rollup, esbuild, Vite) can analyze your import statements and exclude any code that isn’t actually used.
For tree shaking to work:
- Use ES module syntax (
import/export) rather than CommonJS (require/module.exports) - Import specific functions rather than entire libraries
- Ensure packages you use support tree shaking (check their
package.jsonfor amoduleorexportsfield)
Example of tree-shakeable imports:
// Bad: imports entire library
import _ from 'lodash';
_.debounce(fn, 300);
// Good: imports only what's needed
import debounce from 'lodash/debounce';
debounce(fn, 300);
// Best: use a tree-shakeable alternative
import { debounce } from 'lodash-es';
debounce(fn, 300);Code Splitting
Code splitting breaks your JavaScript bundle into smaller chunks that load on demand rather than all upfront.
Route-based splitting: Each page or route gets its own JavaScript bundle. When a user visits the homepage, they only download the homepage’s JavaScript, not the code for every other page.
Component-based splitting: Heavy components (charts, editors, maps) are loaded only when needed.
// Instead of importing upfront
import HeavyChart from './HeavyChart';
// Load on demand
const HeavyChart = React.lazy(() => import('./HeavyChart'));For a typical small business website with five to ten pages, route-based splitting alone can reduce the initial JavaScript payload by 50-70%.
Replace Frameworks with Vanilla JavaScript
For many common web interactions, vanilla JavaScript is all you need:
Mobile navigation toggle:
document.querySelector('.nav-toggle').addEventListener('click', () => {
document.querySelector('.nav-menu').classList.toggle('is-open');
});That’s it. No framework needed.
Smooth scroll to section:
document.querySelectorAll('a[href^="#"]').forEach(link => {
link.addEventListener('click', (e) => {
e.preventDefault();
document.querySelector(link.getAttribute('href')).scrollIntoView({
behavior: 'smooth'
});
});
});Form validation: Native HTML validation attributes (required, type="email", pattern) handle most validation needs without any JavaScript at all.
Animations: CSS transitions and animations handle fades, slides, transforms, and most UI animations. The @starting-style CSS rule now even handles enter/exit animations natively.
For each interaction on your site, ask: can this be done with HTML and CSS alone? If not, can it be done with a few lines of vanilla JavaScript? Only reach for a library or framework when the native approach genuinely can’t handle the requirement.
Defer and Async Non-Critical Scripts
Not every script needs to block rendering:
<!-- Blocks rendering, avoid unless necessary -->
<script src="critical.js"></script>
<!-- Downloads in parallel, executes when ready (may execute before HTML is parsed) -->
<script src="analytics.js" async></script>
<!-- Downloads in parallel, executes after HTML is parsed -->
<script src="enhancements.js" defer></script>For most third-party scripts (analytics, chat widgets, marketing tags), async or defer is appropriate. For your own non-critical JavaScript, defer ensures it runs after the page is parsed.
Performance Budgets
Set a JavaScript budget for your project and enforce it:
These budgets might seem aggressive, but they force good decisions. When you have a 50KB budget, you can’t justify a 45KB framework for a hamburger menu.
We integrate bundle size checking into our CI/CD pipeline. If a commit pushes the JavaScript bundle over budget, the build fails and the developer must find a way to reduce it before merging.
Real-World Impact
We rebuilt a local service company’s website that was running on WordPress with seven active plugins contributing JavaScript. The original site shipped 1.1MB of JavaScript and had a Time to Interactive of 8.2 seconds on mobile.
The rebuilt site used:
- Static HTML with a minimal build step
- Vanilla JavaScript for navigation, form validation, and a simple image gallery
- Deferred loading for analytics
- No jQuery, no framework, no unnecessary plugins
Total JavaScript: 18KB compressed. Time to Interactive: 1.3 seconds on mobile. PageSpeed Insights mobile score: 98.
The site does exactly the same things it did before. It looks the same. The user experience is identical, except it loads in a fraction of the time. That’s what less JavaScript gets you.
The Bottom Line
JavaScript is a tool, and like any tool, the goal is to use exactly as much as you need and no more. For a complex web application, that might be hundreds of kilobytes of well-organized, code-split JavaScript. For a small business website, it might be 20KB of vanilla scripts.
The question to ask isn’t “what framework should we use?” It’s “how much JavaScript does this actually need?” Start from zero and add only what’s justified. Your visitors, your search rankings, and your conversion rates will thank you.