Technical SEO 12 min read

JavaScript Performance Optimization for Better Core Web Vitals

Learn how to optimize JavaScript for better INP and page speed. Reduce bundle size, eliminate long tasks, and improve interactivity.

By Rankture Team
JavaScript Performance Optimization for Better Core Web Vitals

JavaScript is often the biggest bottleneck for page speed and Core Web Vitals. Heavy scripts block rendering, delay interactivity, and frustrate users. Here’s how to optimize JavaScript for better SEO performance.

How JavaScript Affects Core Web Vitals

Impact on INP

INP (Interaction to Next Paint) measures how quickly your page responds to interactions. JavaScript directly affects INP because:

Impact on LCP

JavaScript can delay LCP by:

Impact on CLS

JavaScript can cause CLS when:

Measuring JavaScript Performance

Chrome DevTools Performance Panel

  1. Open DevTools > Performance tab
  2. Click Record and interact with page
  3. Look for long tasks (marked in red)
  4. Identify slow JavaScript functions

Coverage Tab

Find unused JavaScript:

  1. Open DevTools > Coverage tab
  2. Click reload
  3. Red = unused code, Blue = used code
  4. Shows percentage of unused JavaScript

Lighthouse

Provides specific JavaScript recommendations:

Core Optimization Strategies

1. Reduce Bundle Size

Code splitting:

Split your bundle so users only download what they need:

// Instead of importing everything
import { heavyComponent } from './components';

// Import dynamically when needed
const heavyComponent = await import('./components/heavy');

Tree shaking:

Configure your bundler to remove unused exports:

// Bad - imports entire library
import _ from 'lodash';
_.debounce(fn, 100);

// Good - imports only what's needed
import debounce from 'lodash/debounce';
debounce(fn, 100);

Bundle analysis:

Use tools to visualize bundle contents:

2. Defer Non-Critical JavaScript

Use async/defer:

<!-- Blocks rendering - avoid -->
<script src="app.js"></script>

<!-- Downloads in parallel, executes after HTML parsed -->
<script src="app.js" defer></script>

<!-- Downloads in parallel, executes as soon as available -->
<script src="app.js" async></script>

When to use each:

AttributeBest For
NoneInline scripts that must run immediately
asyncIndependent scripts (analytics, ads)
deferScripts that need DOM, order matters

3. Break Up Long Tasks

Any JavaScript task over 50ms is a “long task” that blocks interactivity.

Before (one long task):

function processItems(items) {
  items.forEach(item => {
    // Heavy processing
    heavyOperation(item);
  });
}

After (broken into chunks):

async function processItems(items) {
  const chunks = chunkArray(items, 10);
  
  for (const chunk of chunks) {
    chunk.forEach(item => heavyOperation(item));
    // Yield to browser between chunks
    await scheduler.yield();
  }
}

// Fallback for browsers without scheduler.yield
async function yieldToMain() {
  return new Promise(resolve => {
    setTimeout(resolve, 0);
  });
}

4. Optimize Event Handlers

Debounce/throttle expensive handlers:

// Bad - fires on every scroll
window.addEventListener('scroll', expensiveFunction);

// Good - fires at most every 100ms
window.addEventListener('scroll', throttle(expensiveFunction, 100));

Use passive event listeners:

// Tells browser we won't call preventDefault()
// Allows smooth scrolling
window.addEventListener('scroll', handleScroll, { passive: true });

Move work off the main thread:

// Heavy computation in a Web Worker
const worker = new Worker('worker.js');

worker.postMessage({ data: largeArray });

worker.onmessage = (e) => {
  // Update UI with result
  displayResult(e.data);
};

5. Lazy Load JavaScript Features

Load on interaction:

// Load chart library only when user clicks "Show Chart"
button.addEventListener('click', async () => {
  const { Chart } = await import('chart.js');
  new Chart(canvas, config);
});

Load on visibility:

const observer = new IntersectionObserver((entries) => {
  if (entries[0].isIntersecting) {
    import('./comments.js').then(module => {
      module.init();
    });
    observer.disconnect();
  }
});

observer.observe(document.querySelector('#comments'));

Framework-Specific Optimizations

React

Use React.lazy for code splitting:

const HeavyComponent = React.lazy(() => import('./HeavyComponent'));

function App() {
  return (
    <Suspense fallback={<Loading />}>
      <HeavyComponent />
    </Suspense>
  );
}

Memoize expensive components:

const ExpensiveList = React.memo(({ items }) => {
  return items.map(item => <Item key={item.id} {...item} />);
});

Use startTransition for non-urgent updates:

import { startTransition } from 'react';

function handleChange(e) {
  // Urgent: Update input
  setInputValue(e.target.value);
  
  // Non-urgent: Filter large list
  startTransition(() => {
    setFilteredList(filterLargeList(e.target.value));
  });
}

Vue

Async components:

const HeavyComponent = defineAsyncComponent(() =>
  import('./HeavyComponent.vue')
);

Use v-once for static content:

<span v-once>{{ staticData }}</span>

Next.js/Astro

Use Islands Architecture:

Only hydrate interactive components:

---
// Astro example
import StaticComponent from './Static.astro';
import InteractiveComponent from './Interactive.jsx';
---

<StaticComponent />
<InteractiveComponent client:visible />

Third-Party Script Optimization

Audit Third-Party Scripts

Common heavy third-party scripts:

Delay Third-Party Loading

// Load chat widget after 5 seconds or user interaction
let loaded = false;

function loadChatWidget() {
  if (loaded) return;
  loaded = true;
  
  const script = document.createElement('script');
  script.src = 'https://chat.example.com/widget.js';
  document.body.appendChild(script);
}

setTimeout(loadChatWidget, 5000);
document.addEventListener('scroll', loadChatWidget, { once: true });

Use Facades

Replace heavy embeds with lightweight placeholders:

<!-- Instead of loading entire YouTube player -->
<div class="youtube-facade" data-video="abc123">
  <img src="thumbnail.jpg" alt="Video">
  <button>Play</button>
</div>

<script>
document.querySelector('.youtube-facade').addEventListener('click', (e) => {
  const videoId = e.currentTarget.dataset.video;
  e.currentTarget.innerHTML = `
    <iframe src="https://youtube.com/embed/${videoId}?autoplay=1" 
            frameborder="0" allowfullscreen></iframe>
  `;
});
</script>

Monitoring JavaScript Performance

Real User Monitoring (RUM)

Track actual user JavaScript performance:

// Using web-vitals library
import { onINP, onTBT } from 'web-vitals';

onINP(metric => {
  // Send to analytics
  analytics.track('INP', metric.value);
});

Continuous Monitoring

JavaScript Optimization Checklist

Bundle Size

Loading

Execution

Third-Party

Monitoring


Related Resources:

Tags:

javascript performance inp core web vitals page speed

Share this article:

Ready to improve your SEO?

Get a free SEO audit and see exactly what needs fixing on your site

Start Free Audit