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.
| Mètrica | Sense optimitzar | Amb tècniques bàsiques | Amb optimització avançada |
|---|---|---|---|
| Mida del bundle | 1,2MB | 780KB | 450KB |
| Temps de càrrega | 4,2s | 2,8s | 1,5s |
| Total Blocking Time | 850ms | 340ms | 120ms |
| Ús de memòria | 82MB | 65MB | 48MB |
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>
| Característica | async | defer |
|---|---|---|
| Bloqueja el parsing HTML | No | No |
| Ordre d’execució | No garantit | Mateix ordre del document |
| Moment d’execució | En acabar la descàrrega | Després del parsing HTML |
| Ideal per a | Arxius independents | Arxius 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:
- Els Server Components executen lògica al servidor i envien HTML al client, eliminant el JS d’aquells components del bundle del navegador.
- ESBuild i SWC són compiladors ultraràpids que reemplacen Babel/Webpack en projectes nous, amb bundles més eficients per defecte.
- L’Edge Computing mou l’execució de scripts a nodes edge, reduint la latència de xarxa i descarregant feina del client.
- 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.
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.