Saltar al contingut principal
Desenvolupament 8 min

Optimització JavaScript: Rendiment Web | Blog SEO Ighenatt

Tècniques avançades d'optimització JavaScript per al disseny web professional. Millora la velocitat de càrrega, els Core Web Vitals, el rendiment i l'experiè...

EG

Elu Gonzalez

Autor

La mediana de JavaScript per pàgina en mòbil és 509KB, segons HTTP Archive 2023. I una part important d’aquest pes — entre el 35% i el 45% en llocs mitjans — no s’executa mai durant la primera interacció de l’usuari.

No és un problema de frameworks. És un problema de gestió del bundle. Hem vist llocs amb React que carreguen en 1,2 segons i llocs amb React que triguen 8. La diferència és code splitting, tree shaking i gestió del fil principal. No el framework en si.

L’INP és la mètrica que més pateix. Quan el fil principal del navegador està ocupat executant JavaScript, no pot respondre als clics de l’usuari. Cada Long Task de més de 50ms és un retard perceptible. El que exploem aquí és com eliminar-les.

Impacte de JavaScript en el Rendiment i els Core Web Vitals

Com afecta la velocitat de càrrega

Abans d’abordar les solucions, val la pena entendre exactament com el codi afecta la velocitat.

Els arxius JS són recursos bloquejants del renderitzat per defecte. Quan el navegador troba un arxiu, atura el processament de l’HTML fins que descarrega, analitza i executa aquest codi. L’execució de codi consumeix recursos de CPU, especialment en dispositius mòbils amb capacitats limitades. El codi mal optimitzat també pot causar fuites de memòria que degraden el rendiment progressivament i fins i tot bloqueigen el navegador.

Les mètriques de Core Web Vitals més directament afectades:

  • First Input Delay (FID): Temps que tarda la pàgina a respondre a la primera interacció de l’usuari.
  • Interaction to Next Paint (INP): La nova mètrica que avalua la capacitat de resposta al llarg de tota la visita.
  • Time to Interactive (TTI): Temps fins que la pàgina es torna completament interactiva.
  • Total Blocking Time (TBT): Temps total en què el fil principal està bloquejat.
Comparativa de rendiment: JavaScript optimitzat vs no optimitzat
MètricaSense optimitzarAmb tècniques bàsiquesAmb optimització avançada
Mida del bundle1,2MB780KB450KB
Temps de càrrega4,2s2,8s1,5s
Total Blocking Time850ms340ms120ms
Ús de memòria82MB65MB48MB

Estratègies de Càrrega: Code Splitting i Lazy Loading

Càrrega eficient amb async i defer

Els atributs async i defer controlen quan s’executa l’script en relació amb el parsing de l’HTML:

<!-- Bloqueja l'anàlisi HTML -->
<script src="script.js"></script>

<!-- No bloqueja l'anàlisi HTML, s'executa quan està llest -->
<script async src="script.js"></script>

<!-- No bloqueja l'anàlisi HTML, espera que l'HTML sigui completament analitzat -->
<script defer src="script.js"></script>
Comparació entre atributs async i defer
Característicaasyncdefer
Bloqueja el parsing HTMLNoNo
Ordre d’execucióNo garantitMateix ordre del document
Moment d’execucióEn acabar la descàrregaDesprés del parsing HTML
Ideal per aArxius independentsArxius dependents del DOM

Càrrega dinàmica de codi

Carrega arxius JS només quan siguin necessaris:

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);
  });
}

// Carregar només quan l'usuari interactua
document.querySelector('.boto-feature').addEventListener('click', async () => {
  try {
    await loadScript('/ruta/a/feature-script.js');
    initFeature(); // Funció definida en el script carregat
  } catch (error) {
    console.error('Error en carregar el script:', error);
  }
});

Implementació de code splitting

El code splitting consisteix a dividir el teu codi en chunks més petits que es poden carregar sota demanda.

Amb Webpack

// Importació dinàmica
const boto = document.querySelector('#carregar-component');
boto.addEventListener('click', () => {
  import(/* webpackChunkName: "component" */ './component.js')
    .then(module => {
      const component = module.default;
      component.inicialitzar();
    })
    .catch(error => {
      console.error('Error en carregar el component', error);
    });
});

Amb React

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

// Importació diferida del component pesat
const ComponentPesado = lazy(() => import('./ComponentPesado'));

function App() {
  return (
    <div>
      <h1>La meva aplicació</h1>
      <Suspense fallback={<div>Carregant...</div>}>
        <ComponentPesado />
      </Suspense>
    </div>
  );
}

Reducció del Bundle: Minificació i Compressió

Minificació del codi

Aquest procés elimina espais, comentaris i escurça noms de variables per reduir la mida de l’arxiu. Les eines més usades són Terser (estàndard actual per a JS modern), UglifyJS (per a codi antic) i babel-minify (si ja uses Babel al pipeline).

Exemple amb webpack:

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

module.exports = {
  // ...
  optimization: {
    minimize: true,
    minimizer: [new TerserPlugin({
      terserOptions: {
        compress: {
          drop_console: true, // Elimina console.log en producció
        },
      },
    })],
  },
};

La compressió redueix encara més la mida de transferència. Gzip és àmpliament compatible i té una bona relació compressió/CPU; Brotli ofereix major compressió i és ideal per a text i JavaScript.

Configuració en servidor Apache:

# Habilitar mod_deflate per a compressió Gzip
<IfModule mod_deflate.c>
  AddOutputFilterByType DEFLATE text/html text/plain text/css application/javascript
</IfModule>

# Per a Brotli (requereix mod_brotli)
<IfModule mod_brotli.c>
  AddOutputFilterByType BROTLI_COMPRESS text/html text/plain text/css application/javascript
</IfModule>

Tree shaking i anàlisi del bundle

El tree shaking elimina codi no utilitzat dels teus bundles.

// utils.js
export function funcio1() { /* ... */ }
export function funcio2() { /* ... */ }

// main.js - Només funcio1 serà inclosa en el bundle final
import { funcio1 } from './utils';
funcio1();

Anàlisi del bundle

Eines per analitzar el contingut dels teus bundles:

  • webpack-bundle-analyzer
  • source-map-explorer
# Instal·lar source-map-explorer
npm install --save-dev source-map-explorer

# Analitzar un bundle
npx source-map-explorer dist/main.js

Optimització de l’Execució: Fil Principal i Esdeveniments

Tècniques per alliberar el fil principal

El fil principal del navegador és on ocorre tant el renderitzat com l’execució de JavaScript. Mantenir-lo lliure és la prioritat. Dues tècniques tenen el major impacte:

Descompondre tasques llargues

// En lloc d'aquest codi bloquejant
function processarDadesGrans(dades) {
  const resultats = [];
  for (let i = 0; i < dades.length; i++) {
    resultats.push(processarItem(dades[i]));
  }
  return resultats;
}

// Usar aquest enfocament no bloquejant
function processarDadesGrans(dades, callback) {
  const resultats = [];
  let i = 0;

  function processarLot() {
    const inici = performance.now();

    // Processar fins arribar al temps límit
    while (i < dades.length && performance.now() - inici < 50) {
      resultats.push(processarItem(dades[i]));
      i++;
    }

    // Si hi ha més dades, programar el lot següent
    if (i < dades.length) {
      setTimeout(processarLot, 0);
    } else {
      callback(resultats);
    }
  }

  processarLot();
}

Web Workers per a tasques intensives

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

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

worker.postMessage({dades: arrayGran, operacio: 'processar'});

// worker.js
self.addEventListener('message', function(e) {
  if (e.data.operacio === 'processar') {
    const resultat = processarDades(e.data.dades);
    self.postMessage(resultat);
  }
});

function processarDades(dades) {
  // Operació intensiva que no bloqueja la UI
  return dades.map(x => x * x).filter(x => x > 100);
}

Debounce, throttle i delegació

Els gestors d’esdeveniments mal implementats poden causar problemes de rendiment:

Debounce i throttle per a esdeveniments freqüents:

// Funció debounce - Executa la funció després d'un temps des de l'última crida
function debounce(func, wait) {
  let timeout;
  return function executedFunction(...args) {
    const later = () => {
      clearTimeout(timeout);
      func(...args);
    };
    clearTimeout(timeout);
    timeout = setTimeout(later, wait);
  };
}

// Funció throttle - Limita l'execució a una vegada cada cert temps
function throttle(func, limit) {
  let inThrottle;
  return function(...args) {
    if (!inThrottle) {
      func(...args);
      inThrottle = true;
      setTimeout(() => inThrottle = false, limit);
    }
  };
}

// Aplicació a esdeveniments de desplaçament o redimensionament
window.addEventListener('scroll', debounce(() => {
  // Codi que actualitza alguna cosa basat en el desplaçament
}, 200));

window.addEventListener('resize', throttle(() => {
  // Codi que s'executa durant el redimensionament
}, 300));

Delegació d’esdeveniments:

// En lloc de múltiples listeners
document.querySelectorAll('.item').forEach(item => {
  item.addEventListener('click', handleClick);
});

// Un sol listener amb delegació
document.querySelector('.container').addEventListener('click', (e) => {
  if (e.target.matches('.item')) {
    handleClick.call(e.target, e);
  }
});

Frameworks Moderns: React, Vue i Patró PRPL

Optimitzacions en React

Memoïtzació de components

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

// Memoïtzació de components
const ComponentCostós = memo(function ComponentCostós(props) {
  return <div>{/* Contingut complex */}</div>;
});

function App() {
  // Memoïtzació de valors costosos de calcular
  const dadesProcesades = useMemo(() => {
    return dades.map(processarItem);
  }, [dades]);

  // Memoïtzació de funcions
  const handleClick = useCallback(() => {
    // Lògica del gestor
  }, [dependències]);

  return (
    <div>
      <ComponentCostós dades={dadesProcesades} onClick={handleClick} />
    </div>
  );
}

Virtualització de llistes

import { FixedSizeList } from 'react-window';

function LlistaGran({ 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>
  );
}

Optimitzacions en Vue

Gestió adequada de v-for

<template>
  <!-- Usar key per optimitzar la reactivitat -->
  <div v-for="item in items" :key="item.id">
    {{ item.name }}
  </div>

  <!-- Per a llistes grans, considerar renderitzat condicional -->
  <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: ''
    };
  },
  // Usar computed per a càlculs que requereixen caché
  computed: {
    filteredItems() {
      return this.items.filter(item =>
        item.name.toLowerCase().includes(this.search.toLowerCase())
      );
    }
  },
  // Usar methods per a operacions que sempre necessiten executar-se
  methods: {
    handleClick() {
      // Lògica que sempre ha d'executar-se
    }
  }
};
</script>

Patró PRPL

El patró PRPL és una estratègia per estructurar i servir aplicacions web:

  • Push (o Preload) — Envia/precarrega els recursos crítics
  • Render — Renderitza la ruta inicial el més ràpid possible
  • Pre-cache — Precarrega la resta de rutes
  • Lazy-load — Càrrega diferida d’altres rutes i recursos no crítics

Implementació bàsica:

<!-- Preload de recursos crítics -->
<link rel="preload" href="/css/critical.css" as="style">
<link rel="preload" href="/js/app-core.js" as="script">

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

<!-- Prefetch de rutes probables -->
<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'
      ]);
    })
  );
});

Mesurament i Casos Pràctics

Eines de monitoratge

Per optimitzar eficaçment, cal mesurar.

Eines de diagnòstic

  • Lighthouse: Avaluació integral del rendiment
  • Chrome DevTools Performance panel: Anàlisi detallada de l’execució
  • WebPageTest: Proves de rendiment en diferents condicions

Monitoratge continu en producció

  • Core Web Vitals a Google Search Console: Dades de camp reals de CrUX
  • Firebase Performance Monitoring: Per a aplicacions web i mòbils
  • New Relic, Datadog: Solucions comercials de monitoratge

Mesurar l’impacte de JavaScript en els Core Web Vitals

// Calcular i reportar TBT manualment
let tbtValue = 0;
let lastLongTaskEnd = 0;

// Crear un PerformanceObserver per a Long Tasks
const observer = new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) {
    // Calcular el temps de bloqueig (temps per sobre de 50ms)
    const blockingTime = entry.duration - 50;
    if (blockingTime > 0) {
      tbtValue += blockingTime;
      lastLongTaskEnd = entry.startTime + entry.duration;
    }
  }

  // Enviar a analytics si supera el llindar
  if (tbtValue > 300) {
    analytics.send('poor_tbt', tbtValue);
  }
});

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

Casos d’estudi

Cas 1: E-commerce

TTI de 12s en mòbils 3G amb un bundle JS principal d’1,2MB.

Optimitzacions aplicades: code splitting per rutes i components, lazy loading de carrusels i components per sota del plec, substitució de jQuery per Vanilla JS i preloading estratègic del JS crític.

Resultats: TTI reduït a 4,5s (−62%), bundle inicial a 320KB (−73%) i millora del 28% en taxa de conversió per a mòbil.

Cas 2: Aplicació SPA

FID mitjà de 250ms amb animacions entretallades durant el desplaçament.

Optimitzacions aplicades: migració de tasques pesades a Web Workers, virtualització de llistes llargues, route-based code splitting i optimització de gestors d’esdeveniments amb throttle/debounce.

Resultats: FID reduït a 45ms (−82%), reducció del 95% en Long Tasks durant la navegació i millora del 40% en mètriques d’engagement.

Estratègies segons el tipus de lloc

Llocs de contingut

// Esquema per a llocs de contingut
document.addEventListener('DOMContentLoaded', () => {
  // Carregar immediatament només l'essencial
  loadEssentialFeatures();

  // Diferir funcionalitats no crítiques
  if ('requestIdleCallback' in window) {
    requestIdleCallback(() => {
      loadNonEssentialFeatures();
    });
  } else {
    setTimeout(loadNonEssentialFeatures, 1000);
  }

  // Carregar funcionalitats d'interacció sota demanda
  document.querySelector('.comments-button').addEventListener('click', () => {
    import('./comentaris.js').then(module => {
      module.initializeComments();
    });
  });
});

Aplicacions SPA

// Framework agnostic - Estratègia de càrrega per a SPA
import { registerRoute } from 'workbox-routing';
import { NetworkFirst, CacheFirst, StaleWhileRevalidate } from 'workbox-strategies';

// Crides API amb estratègia Network First (en línia) amb fallback a caché
registerRoute(
  ({url}) => url.pathname.startsWith('/api/'),
  new NetworkFirst({
    cacheName: 'api-cache',
    networkTimeoutSeconds: 3
  })
);

// Recursos estàtics amb Cache First
registerRoute(
  ({request}) => request.destination === 'script' ||
                 request.destination === 'style',
  new CacheFirst({
    cacheName: 'static-resources'
  })
);

// Precàrrega predictiva basada en la navegació de l'usuari
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);
  });
}

Futur de l’Optimització JavaScript

El perfeccionament del codi continuarà sent una part central del desenvolupament web. Algunes tendències a vigilar:

  1. Els Server Components executen lògica al servidor i envien HTML al client, eliminant el JS d’aquells components del bundle del navegador.
  2. ESBuild i SWC són compiladors ultraràpids que reemplacen Babel/Webpack en projectes nous, amb bundles més eficients per defecte.
  3. L’Edge Computing mou l’execució de scripts a nodes edge, reduint la latència de xarxa i descarregant feina del client.
  4. Vite, Parcel i Next.js ja incorporen optimitzacions automàtiques — tree shaking, code splitting, preloading — que abans requerien configuració manual.

La clau per a una eficiència òptima és trobar l’equilibri just entre funcionalitat i lleugeresa. Utilitza el llenguatge de manera estratègica, carregant només el que l’usuari necessita quan ho necessita, i optimitzant tant el lliurament com l’execució del codi. Executa PageSpeed Insights a les deu pàgines amb més trànsit ara mateix. Si alguna dona un LCP per sobre de 2,5 segons o un CLS superior a 0,1, tens un problema concret a resoldre. Digues-nos què has trobat i t’indiquem per on començar.

Comparteix aquest article

Si t'ha resultat útil aquest contingut, comparteix-lo amb els teus col·legues.

Twitter LinkedIn

Preguntes Freqüents

¿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.

Mantén-te actualitzat

Rep al teu email els últims articles, consells i estratègies sobre SEO, rendiment web i màrqueting digital.

Enviem un butlletí cada setmana, i pots donar-te de baixa en qualsevol moment.

Tags: #JavaScript #Optimització #Rendiment #Core Web Vitals #SEO Tècnic
EG

Elu Gonzalez

Expert SEO & Optimització Web