Skip to main content
Development 8 min

JavaScript Optimisation: Web Performance | Blog SEO Ighenatt

Advanced JavaScript optimisation techniques for professional web design. Improve load speed, Core Web Vitals, performance and user experience in your web app...

EG

Elu Gonzalez

Author

The median web page ships 509KB of JavaScript on mobile. That figure from the HTTP Archive does not include what users actually need: it includes what developers added without removing later. Bundle growth tends to be one-directional, and the main thread pays the price.

The part most guides skip is that JavaScript is expensive in ways that go beyond file size. Every kilobyte must be downloaded, parsed, compiled and executed before it becomes functional. On a mid-range Android device, that chain can take three to five times longer than on a developer’s MacBook. What looks fast in DevTools can feel broken in the hands of your actual users.

This guide covers the optimisation techniques that have the most measurable impact on INP, LCP and total blocking time: bundle reduction, deferred loading and moving work off the main thread.

JavaScript optimisation fundamentals

Impact of JavaScript on Performance and Core Web Vitals

How it affects load speed

Before addressing solutions, it is important to understand exactly how code affects speed.

JS files are render-blocking resources by default. When the browser encounters a file, it stops processing the HTML until it downloads, parses and executes that code. Code execution also consumes CPU resources, especially on mobile devices with limited capabilities. Poorly optimised code can cause memory leaks, leading to degraded performance and even browser crashes.

The Core Web Vitals metrics most directly affected:

  • Interaction to Next Paint (INP): Measures responsiveness to user interactions, replacing First Input Delay (FID) as the official metric in March 2024.
  • Time to Interactive (TTI): Time until the page becomes fully interactive.
  • Total Blocking Time (TBT): Total time the main thread is blocked.
Performance comparison: optimised vs non-optimised JavaScript
MetricUnoptimisedWith basic techniquesWith advanced optimisation
Bundle size1.2MB780KB450KB
Load time4.2s2.8s1.5s
Total Blocking Time850ms340ms120ms
Memory usage82MB65MB48MB

Loading Strategies: Code Splitting and Lazy Loading

Efficient loading with async and defer

The async and defer attributes control when the script executes relative to HTML parsing:

<!-- Blocks HTML parsing -->
<script src="script.js"></script>

<!-- Does not block HTML parsing, executes when ready -->
<script async src="script.js"></script>

<!-- Does not block HTML parsing, waits until HTML is completely parsed -->
<script defer src="script.js"></script>
Comparison between async and defer attributes
Featureasyncdefer
Blocks HTML parsingNoNo
Execution orderNot guaranteedSame document order
Execution timingWhen download finishesAfter HTML parsing
Ideal forIndependent filesDOM-dependent files

Dynamic code loading

Load JS files only when needed:

function loadScript(url) {
  return new Promise((resolve, reject) => {
    const script = document.createElement('script');
    script.src = url;
    script.onload = resolve;
    script.onerror = reject;
    document.head.appendChild(script);
  });
}

// Load only when the user interacts
document.querySelector('.feature-button').addEventListener('click', async () => {
  try {
    await loadScript('/path/to/feature-script.js');
    initFeature(); // Function defined in the loaded script
  } catch (error) {
    console.error('Error loading script:', error);
  }
});

Implementing code splitting

Code splitting consists of dividing your code into smaller chunks that can be loaded on demand.

With Webpack

// Dynamic import
const button = document.querySelector('#load-component');
button.addEventListener('click', () => {
  import(/* webpackChunkName: "component" */ './component.js')
    .then(module => {
      const component = module.default;
      component.initialize();
    })
    .catch(error => {
      console.error('Error loading component', error);
    });
});

With React

import React, { lazy, Suspense } from 'react';

// Lazy import of the heavy component
const HeavyComponent = lazy(() => import('./HeavyComponent'));

function App() {
  return (
    <div>
      <h1>My Application</h1>
      <Suspense fallback={<div>Loading...</div>}>
        <HeavyComponent />
      </Suspense>
    </div>
  );
}

Bundle Reduction: Minification and Compression

Code minification

This process removes whitespace, comments and shortens variable names to reduce file size. The most widely used tools are Terser (current standard for modern JS), UglifyJS (for legacy code) and babel-minify (if you already use Babel in the pipeline).

Example with webpack:

// webpack.config.js
const TerserPlugin = require('terser-webpack-plugin');

module.exports = {
  // ...
  optimization: {
    minimize: true,
    minimizer: [new TerserPlugin({
      terserOptions: {
        compress: {
          drop_console: true, // Removes console.log in production
        },
      },
    })],
  },
};

Compression further reduces transfer size. Gzip is widely supported with a good compression/CPU ratio; Brotli offers greater compression and is ideal for text and JavaScript.

Configuration on Apache server:

# Enable mod_deflate for Gzip compression
<IfModule mod_deflate.c>
  AddOutputFilterByType DEFLATE text/html text/plain text/css application/javascript
</IfModule>

# For Brotli (requires mod_brotli)
<IfModule mod_brotli.c>
  AddOutputFilterByType BROTLI_COMPRESS text/html text/plain text/css application/javascript
</IfModule>

Tree shaking and bundle analysis

Tree shaking eliminates unused code from your bundles.

// utils.js
export function function1() { /* ... */ }
export function function2() { /* ... */ }

// main.js - Only function1 will be included in the final bundle
import { function1 } from './utils';
function1();

Bundle analysis

Tools for analysing the content of your bundles:

  • webpack-bundle-analyzer
  • source-map-explorer
# Install source-map-explorer
npm install --save-dev source-map-explorer

# Analyse a bundle
npx source-map-explorer dist/main.js

Execution Optimisation: Main Thread and Events

Techniques for freeing the main thread

The browser’s main thread is where both rendering and JavaScript execution occur. Keeping it free matters. Two techniques have the greatest impact:

Decompose long tasks

// Instead of this blocking code
function processLargeData(data) {
  const results = [];
  for (let i = 0; i < data.length; i++) {
    results.push(processItem(data[i]));
  }
  return results;
}

// Use this non-blocking approach
function processLargeData(data, callback) {
  const results = [];
  let i = 0;

  function processBatch() {
    const start = performance.now();

    // Process until reaching the time limit
    while (i < data.length && performance.now() - start < 50) {
      results.push(processItem(data[i]));
      i++;
    }

    // If there is more data, schedule the next batch
    if (i < data.length) {
      setTimeout(processBatch, 0);
    } else {
      callback(results);
    }
  }

  processBatch();
}

Web Workers for intensive tasks

// main.js
const worker = new Worker('worker.js');

worker.addEventListener('message', function(e) {
  console.log('Result: ', e.data);
});

worker.postMessage({data: largeArray, operation: 'process'});

// worker.js
self.addEventListener('message', function(e) {
  if (e.data.operation === 'process') {
    const result = processData(e.data.data);
    self.postMessage(result);
  }
});

function processData(data) {
  // Intensive operation that does not block the UI
  return data.map(x => x * x).filter(x => x > 100);
}

Debounce, throttle and delegation

Poorly implemented event handlers can cause performance problems:

Debounce and throttle for frequent events:

// Debounce function - Executes the function after a delay from the last call
function debounce(func, wait) {
  let timeout;
  return function executedFunction(...args) {
    const later = () => {
      clearTimeout(timeout);
      func(...args);
    };
    clearTimeout(timeout);
    timeout = setTimeout(later, wait);
  };
}

// Throttle function - Limits execution to once per time interval
function throttle(func, limit) {
  let inThrottle;
  return function(...args) {
    if (!inThrottle) {
      func(...args);
      inThrottle = true;
      setTimeout(() => inThrottle = false, limit);
    }
  };
}

// Application to scroll or resize events
window.addEventListener('scroll', debounce(() => {
  // Code that updates something based on scroll
}, 200));

window.addEventListener('resize', throttle(() => {
  // Code that executes during resize
}, 300));

Event delegation:

// Instead of multiple listeners
document.querySelectorAll('.item').forEach(item => {
  item.addEventListener('click', handleClick);
});

// A single listener with delegation
document.querySelector('.container').addEventListener('click', (e) => {
  if (e.target.matches('.item')) {
    handleClick.call(e.target, e);
  }
});

Modern Frameworks: React, Vue and the PRPL Pattern

Optimisations in React

Component memoisation

import React, { memo, useMemo, useCallback } from 'react';

// Component memoisation
const ExpensiveComponent = memo(function ExpensiveComponent(props) {
  return <div>{/* Complex content */}</div>;
});

function App() {
  // Memoisation of expensive-to-calculate values
  const processedData = useMemo(() => {
    return data.map(processItem);
  }, [data]);

  // Function memoisation
  const handleClick = useCallback(() => {
    // Handler logic
  }, [dependencies]);

  return (
    <div>
      <ExpensiveComponent data={processedData} onClick={handleClick} />
    </div>
  );
}

List virtualisation

import { FixedSizeList } from 'react-window';

function LargeList({ items }) {
  const Row = ({ index, style }) => (
    <div style={style}>
      {items[index].text}
    </div>
  );

  return (
    <FixedSizeList
      height={400}
      width="100%"
      itemCount={items.length}
      itemSize={35}
    >
      {Row}
    </FixedSizeList>
  );
}

Optimisations in Vue

Proper handling of v-for

<template>
  <!-- Use key to optimise reactivity -->
  <div v-for="item in items" :key="item.id">
    {{ item.name }}
  </div>

  <!-- For large lists, consider conditional rendering -->
  <virtual-list :items="items" :height="400" :item-height="40">
    <template v-slot:item="{ item }">
      {{ item.name }}
    </template>
  </virtual-list>
</template>

Computed properties vs Methods

<script>
export default {
  data() {
    return {
      items: [...],
      search: ''
    };
  },
  // Use computed for calculations that require caching
  computed: {
    filteredItems() {
      return this.items.filter(item =>
        item.name.toLowerCase().includes(this.search.toLowerCase())
      );
    }
  },
  // Use methods for operations that always need to execute
  methods: {
    handleClick() {
      // Logic that always must execute
    }
  }
};
</script>

PRPL Pattern

The PRPL pattern is a strategy for structuring and serving web applications:

  • Push (or Preload): Send/preload critical resources
  • Render: Render the initial route as quickly as possible
  • Pre-cache: Preload the remaining routes
  • Lazy-load: Lazy load other routes and non-critical resources

Basic implementation:

<!-- Preload of critical resources -->
<link rel="preload" href="/css/critical.css" as="style">
<link rel="preload" href="/js/app-core.js" as="script">

<!-- Precache with Service Worker -->
<script>
  if ('serviceWorker' in navigator) {
    window.addEventListener('load', () => {
      navigator.serviceWorker.register('/sw.js');
    });
  }
</script>

<!-- Prefetch of probable routes -->
<link rel="prefetch" href="/js/about-page.js">
// sw.js (Service Worker)
self.addEventListener('install', (event) => {
  event.waitUntil(
    caches.open('app-shell-v1').then((cache) => {
      return cache.addAll([
        '/',
        '/css/critical.css',
        '/js/app-core.js',
        '/offline.html'
      ]);
    })
  );
});

Measurement and Practical Cases

Monitoring tools

To optimise effectively, you need to measure.

Diagnostic tools

  • Lighthouse: Comprehensive performance assessment
  • Chrome DevTools Performance panel: Detailed execution analysis
  • WebPageTest: Performance tests under different conditions

Continuous production monitoring

  • Core Web Vitals in Google Search Console: Real field data from CrUX
  • Firebase Performance Monitoring: For web and mobile applications
  • New Relic, Datadog: Commercial monitoring solutions

Measuring the impact of JavaScript on Core Web Vitals

// Manually calculate and report TBT
let tbtValue = 0;
let lastLongTaskEnd = 0;

// Create a PerformanceObserver for Long Tasks
const observer = new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) {
    // Calculate blocking time (time above 50ms)
    const blockingTime = entry.duration - 50;
    if (blockingTime > 0) {
      tbtValue += blockingTime;
      lastLongTaskEnd = entry.startTime + entry.duration;
    }
  }

  // Send to analytics if threshold exceeded
  if (tbtValue > 300) {
    analytics.send('poor_tbt', tbtValue);
  }
});

observer.observe({entryTypes: ['longtask']});

Case studies

Case 1: E-commerce

TTI of 12s on 3G mobile with a 1.2MB main JS bundle.

Optimisations applied: code splitting by routes and components, lazy loading of carousels and below-the-fold components, replacing jQuery with Vanilla JS, and strategic preloading of critical JS.

Results: TTI reduced to 4.5s (−62%), initial bundle to 320KB (−73%) and 28% improvement in mobile conversion rate.

Case 2: SPA Application

Average INP of 380ms with choppy animations during scroll.

Optimisations applied: migrating heavy tasks to Web Workers, virtualising long lists, route-based code splitting, and event handler optimisation with throttle/debounce.

Results: INP reduced to 85ms (−78%), within the good threshold (≤200ms); 95% reduction in Long Tasks during navigation; 40% improvement in engagement metrics.

Strategies by site type

Content sites

// Pattern for content sites
document.addEventListener('DOMContentLoaded', () => {
  // Immediately load only essentials
  loadEssentialFeatures();

  // Defer non-critical functionality
  if ('requestIdleCallback' in window) {
    requestIdleCallback(() => {
      loadNonEssentialFeatures();
    });
  } else {
    setTimeout(loadNonEssentialFeatures, 1000);
  }

  // Load interaction functionality on demand
  document.querySelector('.comments-button').addEventListener('click', () => {
    import('./comments.js').then(module => {
      module.initializeComments();
    });
  });
});

SPA applications

// Framework agnostic - Loading strategy for SPA
import { registerRoute } from 'workbox-routing';
import { NetworkFirst, CacheFirst, StaleWhileRevalidate } from 'workbox-strategies';

// API calls with Network First strategy (online) with cache fallback
registerRoute(
  ({url}) => url.pathname.startsWith('/api/'),
  new NetworkFirst({
    cacheName: 'api-cache',
    networkTimeoutSeconds: 3
  })
);

// Static resources with Cache First
registerRoute(
  ({request}) => request.destination === 'script' ||
                 request.destination === 'style',
  new CacheFirst({
    cacheName: 'static-resources'
  })
);

// Predictive preloading based on user navigation
function preloadRoutes(currentRoute) {
  const likelyNextRoutes = predictNextRoutes(currentRoute);
  likelyNextRoutes.forEach(route => {
    const link = document.createElement('link');
    link.rel = 'prefetch';
    link.href = `/js/chunks/${route}.js`;
    document.head.appendChild(link);
  });
}

Future of JavaScript Optimisation

Code optimisation will remain a core part of web development. With the growing complexity of web applications and the evolution of standards, some trends to watch:

  1. Server Components run logic on the server and send HTML to the client, removing those components’ JS from the browser bundle entirely.
  2. ESBuild and SWC are ultra-fast compilers replacing Babel/Webpack in new projects, producing more efficient bundles by default.
  3. Edge Computing moves script execution to edge nodes, reducing network latency and offloading work from the client.
  4. Vite, Parcel and Next.js already incorporate automatic optimisations — tree shaking, code splitting, preloading — that previously required manual configuration.

The key to optimal efficiency is finding the right balance between functionality and lightness. Use the language strategically, loading only what the user needs when they need it, and optimising both the delivery and execution of code. Run PageSpeed Insights on your ten highest-traffic pages right now. If any return an LCP above 2.5 seconds or a CLS above 0.1, you have a specific problem to solve. Tell us what you found and we will tell you where to start.

Share this article

If you found this content useful, share it with your colleagues.

Frequently Asked Questions

¿Google puede rastrear sitios web con mucho JavaScript?

Sí, Google puede rastrear JavaScript, pero con limitaciones. Para optimizar usa server-side rendering, implementa hydration progresiva, asegura que el contenido crítico sea accesible sin JS y usa técnicas como lazy loading responsable.

¿Qué es mejor para SEO: SPA o páginas tradicionales?

Las páginas tradicionales suelen ser mejores para SEO por su facilidad de crawling. Las SPAs requieren configuración adicional (SSR, prerendering) pero pueden ofrecer mejor UX. La elección depende de tus objetivos específicos y recursos técnicos.

¿Con qué frecuencia publican contenido nuevo?

Publicamos artículos nuevos semanalmente, enfocados en las últimas tendencias de SEO técnico, casos de estudio reales y mejores prácticas. Suscríbete a nuestro newsletter para no perderte ninguna actualización.

¿Los consejos son aplicables a cualquier tipo de sitio web?

Nuestros consejos se adaptan a diferentes tipos de sitios: ecommerce, blogs, sitios corporativos y aplicaciones web. Siempre indicamos cuándo una técnica es específica para cierto tipo de sitio o requerimientos técnicos.

Stay updated

Receive the latest articles, tips and strategies about SEO, web performance and digital marketing in your email.

We send a newsletter every week, and you can unsubscribe at any time.

Tags: #JavaScript #Optimisation #Performance #Core Web Vitals #Technical SEO
EG

Elu Gonzalez

SEO Expert & Web Optimization