JavaScript Performance Optimization: Loading, Parsing, and Executing
Why JavaScript Performance Matters
JavaScript is the most expensive resource your page loads. Unlike images that decode progressively, JavaScript must be downloaded, parsed, compiled, and executed before it does anything. On mobile devices, parsing 200 KB of JavaScript can take longer than downloading 1 MB of images.
Every millisecond of blocked main-thread time delays interactivity. Google's Core Web Vitals — specifically Interaction to Next Paint (INP) — directly measure how quickly your page responds to user input. Slow JavaScript is the most common cause of poor INP scores.
Loading Strategies
How you include a <script> tag determines when the browser downloads and executes it. Choose the right strategy for each script.
Default Blocking
<script src="app.js"></script>
The browser stops parsing HTML, downloads the script, and executes it before continuing. This creates a visible pause — the page appears blank until the script finishes.
Use for: Nothing, realistically. Default scripts rarely make sense in modern pages.
Async
<script src="analytics.js" async></script>
The browser downloads the script in parallel with HTML parsing. Once downloaded, it pauses parsing to execute. Multiple async scripts execute in whatever order they finish downloading — not necessarily the order they appear in HTML.
Use for: Independent scripts like analytics, ads, or chat widgets that do not depend on your application code.
Defer
<script src="app.js" defer></script>
The browser downloads the script in parallel with HTML parsing but waits until parsing finishes before executing. Deferred scripts run in document order.
Use for: Application code that needs the DOM ready but does not need to block rendering.
ES Module Scripts
<script type="module" src="app.js"></script>
Module scripts behave like defer by default — they download in parallel and execute after parsing. Adding async to a module script means it executes as soon as it downloads, useful for modules that are truly independent.
| Attribute | Downloads | Executes | Order |
|---|---|---|---|
| (none) | Blocks HTML | Immediately | Document order |
async | Parallel | When ready | Download order |
defer | Parallel | After parse | Document order |
type="module" | Parallel | After parse | Document order |
Parsing and Compilation
V8 (Chrome), SpiderMonkey (Firefox), and JavaScriptCore (Safari) all parse and compile JavaScript before executing it. This cost is proportional to script size.
Reduce Parse-Heavy Code
- Avoid large polyfills — use
@babel/preset-envwithuseBuiltIns: 'usage'to include only what you need - Prefer native APIs —
Array.prototype.at(),structuredClone(), andIntl.Segmenterreduce the need for libraries - Lazy-load non-critical code — code-split below-the-fold features so the initial parse is smaller
Preload Critical Scripts
<link rel="preload" href="critical.js" as="script" />
Preloading tells the browser to start downloading early, before it discovers the <script> tag in the HTML. This is useful for scripts injected dynamically or loaded from a different origin.
Code Splitting
Code splitting breaks your bundle into smaller chunks loaded on demand. Instead of shipping 500 KB upfront, you send 100 KB immediately and load the rest when the user navigates to a new section.
Route-Based Splitting
// Vite / Vue Router
const routes = [
{
path: '/dashboard',
component: () => import('./views/Dashboard.vue'),
},
{
path: '/settings',
component: () => import('./views/Settings.vue'),
},
];
Each route becomes a separate chunk. The user only downloads the code for the page they visit.
Component-Based Splitting
// React
const HeavyChart = lazy(() => import('./components/HeavyChart'));
function App() {
return (
<Suspense fallback={<Spinner />}>
<HeavyChart />
</Suspense>
);
}
Wrap heavy components — charts, editors, video players — in lazy() and Suspense. They load only when rendered.
Vendor Splitting
Separate third-party libraries into their own chunk. These change less frequently than your application code, so they stay cached longer between deployments.
// Vite
export default {
build: {
rollupOptions: {
output: {
manualChunks: {
vendor: ['vue', 'vue-router', 'pinia'],
},
},
},
},
};
Web Workers
JavaScript runs on a single main thread. When a complex calculation blocks that thread, the page freezes. Web Workers move expensive work to a background thread.
// main.js
const worker = new Worker('processor.js');
worker.postMessage({ data: largeDataset });
worker.onmessage = (event) => {
renderResults(event.data);
};
// processor.js
self.onmessage = (event) => {
const result = heavyComputation(event.data);
self.postMessage(result);
};
Workers cannot access the DOM directly — they communicate through postMessage. Use them for:
- JSON parsing of large payloads (10 MB+)
- Image or audio processing
- Cryptographic operations
- Data transformations and sorting
Comlink: A Simpler Worker API
The Comlink library abstracts postMessage behind a clean async interface:
import * as Comlink from 'comlink';
const worker = Comlink.wrap(new Worker('processor.js'));
const result = await worker.heavyComputation(data);
Performance Monitoring
You cannot optimize what you do not measure. Use these tools to identify bottlenecks.
Browser DevTools
- Performance tab: Record a trace to see main-thread activity, scripting time, and layout shifts
- Coverage tab: Find JavaScript that loads but never executes — prime candidates for code splitting
- Lighthouse: Audit your page for performance, accessibility, and SEO scores
Web Vitals Library
import { onINP, onLCP, onCLS } from 'web-vitals';
onINP((metric) => reportToAnalytics(metric));
onLCP((metric) => reportToAnalytics(metric));
onCLS((metric) => reportToAnalytics(metric));
Send real-user metrics to your analytics service. Lab data (Lighthouse) does not always match field data (real users on slow networks).
###bundlewatch and Size Limit
Add size budgets to CI so bundles never grow silently:
{
"scripts": {
"size": "size-limit"
},
"size-limit": [
{ "path": "dist/app.js", "limit": "80 KB" }
]
}
If a PR adds code that pushes the bundle over 80 KB, the build fails.
Checklist for Faster JavaScript
- Use
deferortype="module"for all non-critical scripts - Code-split routes and heavy components
- Extract vendor libraries into a separate chunk for better caching
- Move CPU-intensive work to Web Workers
- Set bundle size limits in CI
- Monitor Core Web Vitals from real users
- Tree-shake and minify every production build
Key Takeaways
- JavaScript blocks the main thread — loading strategy directly impacts perceived performance
deferis the safe default;asyncis for independent scripts only- Code splitting reduces initial parse time by loading features on demand
- Web Workers move heavy computation off the main thread
- Measure both lab data (Lighthouse) and field data (real users) to catch regressions early
Try It Yourself
Before deploying, minify your JavaScript with our free JS Minifier to see the size difference. Smaller scripts parse faster, download quicker, and improve every Core Web Vitals metric.
Related Guides
- JavaScript Minification Guide — How whitespace removal and mangling reduce file size
- Tree Shaking vs Minification — Eliminate unused code before minifying for maximum savings
- CSS Minification Guide — Apply the same optimization principles to your stylesheets