Optimización de Scripts
Scripts de terceros (Google Analytics, Facebook Pixel, chat widgets) pueden hacer tu sitio lento si no los cargas correctamente. NextJS tiene el componente Script
para optimizarlos.
El problema con <script>
<!-- ❌ Tag HTML tradicional -->
<script src="https://www.googletagmanager.com/gtag/js?id=GA_ID"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', 'GA_ID');
</script>
Problemas:
- Bloquea el renderizado (página lenta)
- No hay control de cuándo se carga
- Puede duplicarse en navegación cliente
- Dificulta medir el impacto en performance
La solución: <Script>
import Script from 'next/script'
export default function RootLayout({ children }) {
return (
<html>
<body>
{children}
<Script
src="https://www.googletagmanager.com/gtag/js?id=GA_ID"
strategy="afterInteractive"
/>
<Script id="google-analytics" strategy="afterInteractive">
{`
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', 'GA_ID');
`}
</Script>
</body>
</html>
)
}
NextJS controla cuándo y cómo se cargan los scripts para no afectar la performance.
Estrategias de carga
La prop strategy
controla cuándo se carga el script:
beforeInteractive
Carga antes de que la página sea interactiva. Para scripts críticos.
<Script
src="/polyfills.js"
strategy="beforeInteractive"
/>
Cuándo usar:
- Polyfills críticos
- Scripts que deben ejecutarse antes que todo
- Bot detection
- A/B testing que afecta el primer render
Importante: Solo funciona en app/layout.tsx
, no en páginas individuales.
afterInteractive (default)
Carga después de que la página sea interactiva. Para la mayoría de scripts.
<Script
src="https://www.googletagmanager.com/gtag/js"
strategy="afterInteractive"
/>
Cuándo usar:
- Analytics (Google Analytics, Mixpanel)
- Ads
- Social widgets
- Chat widgets
- La mayoría de scripts de terceros
Por qué es mejor: El usuario puede interactuar con tu página mientras los scripts cargan en background.
lazyOnload
Carga cuando el navegador está idle (no tiene nada más que hacer).
<Script
src="https://connect.facebook.net/en_US/sdk.js"
strategy="lazyOnload"
/>
Cuándo usar:
- Scripts no críticos
- Widgets de redes sociales
- Comentarios (Disqus)
- Customer support chats
- Scripts que no afectan la funcionalidad principal
Por qué es mejor: Estos scripts no compiten por recursos con el contenido principal.
worker (experimental)
Carga el script en un Web Worker (no bloquea el thread principal).
<Script
src="/heavy-calculation.js"
strategy="worker"
/>
Experimental - No recomendado para producción aún.
Ejemplos prácticos
Google Analytics
// app/layout.tsx
import Script from 'next/script'
export default function RootLayout({ children }) {
return (
<html>
<body>
{children}
<Script
src={`https://www.googletagmanager.com/gtag/js?id=${process.env.NEXT_PUBLIC_GA_ID}`}
strategy="afterInteractive"
/>
<Script id="google-analytics" strategy="afterInteractive">
{`
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', '${process.env.NEXT_PUBLIC_GA_ID}');
`}
</Script>
</body>
</html>
)
}
Google Tag Manager
<Script
id="gtm"
strategy="afterInteractive"
dangerouslySetInnerHTML={{
__html: `
(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':
new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],
j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);
})(window,document,'script','dataLayer','${process.env.NEXT_PUBLIC_GTM_ID}');
`
}}
/>
Facebook Pixel
<Script
id="facebook-pixel"
strategy="afterInteractive"
>
{`
!function(f,b,e,v,n,t,s)
{if(f.fbq)return;n=f.fbq=function(){n.callMethod?
n.callMethod.apply(n,arguments):n.queue.push(arguments)};
if(!f._fbq)f._fbq=n;n.push=n;n.loaded=!0;n.version='2.0';
n.queue=[];t=b.createElement(e);t.async=!0;
t.src=v;s=b.getElementsByTagName(e)[0];
s.parentNode.insertBefore(t,s)}(window, document,'script',
'https://connect.facebook.net/en_US/fbevents.js');
fbq('init', '${process.env.NEXT_PUBLIC_FB_PIXEL_ID}');
fbq('track', 'PageView');
`}
</Script>
Chat widget (Intercom)
<Script
id="intercom"
strategy="lazyOnload"
>
{`
window.intercomSettings = {
app_id: "${process.env.NEXT_PUBLIC_INTERCOM_ID}"
};
(function(){var w=window;var ic=w.Intercom;if(typeof ic==="function"){
ic('reattach_activator');ic('update',w.intercomSettings);}else{var d=document;
var i=function(){i.c(arguments);};i.q=[];i.c=function(args){i.q.push(args);};
w.Intercom=i;var l=function(){var s=d.createElement('script');s.type='text/javascript';
s.async=true;s.src='https://widget.intercom.io/widget/${process.env.NEXT_PUBLIC_INTERCOM_ID}';
var x=d.getElementsByTagName('script')[0];x.parentNode.insertBefore(s,x);};
if(document.readyState==='complete'){l();}else if(w.attachEvent){
w.attachEvent('onload',l);}else{w.addEventListener('load',l,false);}}})();
`}
</Script>
Stripe
<Script
src="https://js.stripe.com/v3/"
strategy="afterInteractive"
onLoad={() => {
console.log('Stripe cargado')
}}
/>
Mapbox
<Script
src="https://api.mapbox.com/mapbox-gl-js/v2.15.0/mapbox-gl.js"
strategy="afterInteractive"
/>
Eventos del ciclo de vida
Ejecuta código cuando el script carga:
onLoad
<Script
src="https://example.com/script.js"
strategy="afterInteractive"
onLoad={() => {
console.log('Script cargado correctamente')
// Inicializar librería
window.exampleLib.init()
}}
/>
onReady
<Script
src="https://example.com/script.js"
strategy="afterInteractive"
onReady={() => {
console.log('Script listo para usar')
}}
/>
onError
<Script
src="https://example.com/script.js"
strategy="afterInteractive"
onError={(e) => {
console.error('Error cargando script:', e)
}}
/>
Scripts inline
Para código JavaScript inline:
<Script id="inline-script" strategy="afterInteractive">
{`
console.log('Este código se ejecuta inline');
window.miVariable = 'valor';
`}
</Script>
Importante: Scripts inline requieren un id
único.
Componente reutilizable
Crea un componente para scripts comunes:
// components/GoogleAnalytics.tsx
import Script from 'next/script'
export default function GoogleAnalytics({ gaId }: { gaId: string }) {
return (
<>
<Script
src={`https://www.googletagmanager.com/gtag/js?id=${gaId}`}
strategy="afterInteractive"
/>
<Script id="google-analytics" strategy="afterInteractive">
{`
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', '${gaId}');
`}
</Script>
</>
)
}
// app/layout.tsx
import GoogleAnalytics from '@/components/GoogleAnalytics'
export default function RootLayout({ children }) {
return (
<html>
<body>
{children}
<GoogleAnalytics gaId={process.env.NEXT_PUBLIC_GA_ID!} />
</body>
</html>
)
}
Scripts por entorno
Solo carga en producción:
export default function RootLayout({ children }) {
return (
<html>
<body>
{children}
{process.env.NODE_ENV === 'production' && (
<Script
src="https://www.googletagmanager.com/gtag/js"
strategy="afterInteractive"
/>
)}
</body>
</html>
)
}
Scripts condicionales
Solo carga si el usuario aceptó cookies:
'use client'
import { useState } from 'react'
import Script from 'next/script'
export default function ConsentManager() {
const [accepted, setAccepted] = useState(false)
return (
<>
{!accepted && (
<div className="cookie-banner">
<p>Usamos cookies para analytics</p>
<button onClick={() => setAccepted(true)}>
Aceptar
</button>
</div>
)}
{accepted && (
<Script
src="https://www.googletagmanager.com/gtag/js"
strategy="afterInteractive"
/>
)}
</>
)
}
Patrones avanzados
Lazy loading de librería pesada
'use client'
import { useState } from 'react'
import Script from 'next/script'
export default function Map() {
const [mapLoaded, setMapLoaded] = useState(false)
const [showMap, setShowMap] = useState(false)
return (
<>
<button onClick={() => setShowMap(true)}>
Ver mapa
</button>
{showMap && (
<>
<Script
src="https://api.mapbox.com/mapbox-gl-js/v2.15.0/mapbox-gl.js"
strategy="lazyOnload"
onLoad={() => setMapLoaded(true)}
/>
{mapLoaded ? (
<div id="map"></div>
) : (
<div>Cargando mapa...</div>
)}
</>
)}
</>
)
}
Múltiples scripts dependientes
'use client'
import { useState } from 'react'
import Script from 'next/script'
export default function StripeCheckout() {
const [stripeLoaded, setStripeLoaded] = useState(false)
return (
<>
<Script
src="https://js.stripe.com/v3/"
strategy="afterInteractive"
onLoad={() => setStripeLoaded(true)}
/>
{stripeLoaded && (
<Script id="stripe-init">
{`
const stripe = Stripe('${process.env.NEXT_PUBLIC_STRIPE_KEY}');
// Inicializar Stripe
`}
</Script>
)}
</>
)
}
Script con datos del servidor
// app/layout.tsx
export default async function RootLayout({ children }) {
const config = await fetchConfig()
return (
<html>
<body>
{children}
<Script id="config" strategy="beforeInteractive">
{`window.APP_CONFIG = ${JSON.stringify(config)};`}
</Script>
</body>
</html>
)
}
Comparación de estrategias
Estrategia | Cuándo carga | Uso típico | Performance |
---|---|---|---|
beforeInteractive | Antes de hidratación | Polyfills, A/B testing | ⚠️ Bloquea render inicial |
afterInteractive | Después de hidratación | Analytics, ads | ✅ Bueno |
lazyOnload | Cuando browser idle | Widgets, chats | ✅✅ Excelente |
worker | En Web Worker | Cálculos pesados | ✅✅✅ Experimental |
Mejores prácticas
1. Usa la estrategia correcta
// ✅ Analytics: afterInteractive
<Script src="/analytics.js" strategy="afterInteractive" />
// ✅ Chat: lazyOnload
<Script src="/chat.js" strategy="lazyOnload" />
// ✅ Polyfill crítico: beforeInteractive (solo en layout)
<Script src="/polyfill.js" strategy="beforeInteractive" />
2. Agrupa scripts relacionados
// components/Analytics.tsx
export default function Analytics() {
return (
<>
<Script src="/ga.js" strategy="afterInteractive" />
<Script src="/fb-pixel.js" strategy="afterInteractive" />
<Script src="/hotjar.js" strategy="lazyOnload" />
</>
)
}
3. Usa variables de entorno
// ✅ Bien
<Script src={`https://api.com/script.js?key=${process.env.NEXT_PUBLIC_KEY}`} />
// ❌ Mal: API key hardcodeada
<Script src="https://api.com/script.js?key=abc123" />
4. Coloca scripts en layout
// ✅ Bien: En layout (una vez)
// app/layout.tsx
<Script src="/analytics.js" strategy="afterInteractive" />
// ❌ Mal: En cada página (se duplica)
// app/page.tsx
<Script src="/analytics.js" strategy="afterInteractive" />
5. Considera la privacidad
// Solo carga si el usuario acepta
{userAcceptedCookies && (
<Script src="/tracking.js" strategy="afterInteractive" />
)}
Medir impacto
Con Lighthouse
# Chrome DevTools → Lighthouse
# Métricas importantes:
# - Total Blocking Time (TBT)
# - First Contentful Paint (FCP)
# - Time to Interactive (TTI)
Con Web Vitals
// app/layout.tsx
import { SpeedInsights } from '@vercel/speed-insights/next'
export default function RootLayout({ children }) {
return (
<html>
<body>
{children}
<SpeedInsights />
</body>
</html>
)
}
Vercel Analytics te muestra el impacto de tus scripts en tiempo real.
Errores comunes
Error: Script duplicado
// ❌ Mal: Script en layout Y en página
// app/layout.tsx
<Script src="/analytics.js" />
// app/page.tsx
<Script src="/analytics.js" /> // Se carga 2 veces
Solución: Scripts globales solo en layout.tsx
.
Error: Sin id en inline script
// ❌ Error: Falta id
<Script>
{`console.log('hola')`}
</Script>
// ✅ Correcto
<Script id="mi-script">
{`console.log('hola')`}
</Script>
Error: beforeInteractive en página
// ❌ Error: beforeInteractive solo funciona en layout
// app/page.tsx
<Script src="/script.js" strategy="beforeInteractive" />
// ✅ Correcto: En layout
// app/layout.tsx
<Script src="/script.js" strategy="beforeInteractive" />
Scripts de terceros populares
Hotjar
<Script id="hotjar" strategy="afterInteractive">
{`
(function(h,o,t,j,a,r){
h.hj=h.hj||function(){(h.hj.q=h.hj.q||[]).push(arguments)};
h._hjSettings={hjid:${process.env.NEXT_PUBLIC_HOTJAR_ID},hjsv:6};
a=o.getElementsByTagName('head')[0];
r=o.createElement('script');r.async=1;
r.src=t+h._hjSettings.hjid+j+h._hjSettings.hjsv;
a.appendChild(r);
})(window,document,'https://static.hotjar.com/c/hotjar-','.js?sv=');
`}
</Script>
Mixpanel
<Script id="mixpanel" strategy="afterInteractive">
{`
(function(f,b){
if(!b.__SV){
window.mixpanel=b;
var a=f.getElementsByTagName("script")[0];
var e=f.createElement("script");
e.async=!0;
e.src="https://cdn.mxpnl.com/libs/mixpanel-2-latest.min.js";
a.parentNode.insertBefore(e,a);
}
})(document,window.mixpanel||[]);
mixpanel.init("${process.env.NEXT_PUBLIC_MIXPANEL_TOKEN}");
`}
</Script>
Crisp Chat
<Script id="crisp" strategy="lazyOnload">
{`
window.$crisp=[];
window.CRISP_WEBSITE_ID="${process.env.NEXT_PUBLIC_CRISP_ID}";
(function(){
d=document;
s=d.createElement("script");
s.src="https://client.crisp.chat/l.js";
s.async=1;
d.getElementsByTagName("head")[0].appendChild(s);
})();
`}
</Script>
Resumen
Componente Script de NextJS:
- Controla cuándo se cargan scripts de terceros
- Tres estrategias: beforeInteractive, afterInteractive, lazyOnload
- Previene bloqueo del render
- Eventos del ciclo de vida (onLoad, onError)
- No duplica scripts en navegación cliente
Estrategias:
beforeInteractive
: Scripts críticos (raro)afterInteractive
: Analytics, ads (común)lazyOnload
: Widgets, chats (recomendado para no críticos)
Regla de oro:
Usa afterInteractive
para analytics, lazyOnload
para todo lo demás. Coloca scripts globales en layout.tsx
.