JavaScript Performance Optimization: Loading, Parsing, and Executing

May 28, 20267 min read

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.

AttributeDownloadsExecutesOrder
(none)Blocks HTMLImmediatelyDocument order
asyncParallelWhen readyDownload order
deferParallelAfter parseDocument order
type="module"ParallelAfter parseDocument 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-env with useBuiltIns: 'usage' to include only what you need
  • Prefer native APIsArray.prototype.at(), structuredClone(), and Intl.Segmenter reduce 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

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 defer or type="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
  • defer is the safe default; async is 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.