Diferir scripts hasta que sean necesarios

Carga JavaScript bajo demanda usando IntersectionObserver y disparadores de interacción del usuario

Arjen Karel Core Web Vitals Consultant
Arjen Karel - linkedin
Last update: 2026-03-05

Diferir scripts hasta que sean necesarios

La página móvil mediana envía 251 KB de JavaScript no utilizado según el Web Almanac 2025. Eso es JavaScript que el navegador descarga, analiza y compila antes de que un visitante lo necesite. Formularios en los que nadie ha hecho clic. Widgets de chat que nadie ha abierto. Integraciones de mapas que están debajo del pliegue. Todo compitiendo por ancho de banda y tiempo de CPU durante la fase más crítica de la carga de la página.

La forma más efectiva de lidiar con esto es no cargar scripts hasta que realmente se necesiten. Esto es diferente de usar el atributo async o defer en una etiqueta de script. Esos atributos aún descargan el script durante la carga de la página; solo cambian cuándo se ejecuta. La carga bajo demanda no descarga el script en absoluto hasta que se activa un disparador.

Última revisión por Arjen Karel en marzo de 2026

Hemos estado haciendo esto con imágenes durante mucho tiempo. Se llama lazy loading. Con lazy loading, una imagen debajo del pliegue se carga justo antes de que entre en la vista al hacer scroll. El navegador puede dedicar sus recursos a descargar, analizar y pintar las cosas que realmente se necesitan. El mismo principio se aplica a JavaScript, y solucionará la advertencia de Lighthouse "reduce unused JavaScript" y mejorará las métricas de responsividad como Interaction to Next Paint (INP).

Desafortunadamente, no es tan simple como agregar loading="lazy" a una imagen, pero con una pequeña función auxiliar y un disparador podemos hacerlo funcionar.

La función auxiliar de inyección de scripts

Para agregar scripts a la página después de la carga, necesitamos una pequeña función que cree un elemento de script y lo añada al head del documento.

function injectScript(scriptUrl, callback) {
    const script = document.createElement('script');
    script.src = scriptUrl;
    if (typeof callback === 'function') {
        script.onload = callback;
    }
    document.head.appendChild(script);
}

El parámetro scriptUrl es la URL del script a cargar. La función callback opcional se ejecuta después de que el script termine de cargarse. Esto es importante para scripts que necesitan inicialización, como llamar a initMap() después de cargar la API de Google Maps.

Activar la carga bajo demanda

Con la función auxiliar de inyección lista, necesitamos un disparador. Hay dos métodos fiables: cargar cuando un elemento entra en la vista al hacer scroll y cargar cuando el usuario interactúa con un elemento.

IntersectionObserver: cargar cuando sea visible

El IntersectionObserver se activa cuando un elemento entra en el viewport. Este es el disparador adecuado para scripts vinculados a una sección específica de la página: un contenedor de mapa, una sección de comentarios o un widget incrustado debajo del pliegue.

function injectScriptOnIntersection(scriptUrl, elementSelector, callback) {
    const element = document.querySelector(elementSelector);
    const observer = new IntersectionObserver((entries, obs) => {
        entries.forEach(entry => {
            if (entry.isIntersecting) {
                injectScript(scriptUrl, callback);
                obs.unobserve(entry.target);
            }
        });
    });
    observer.observe(element);
}

La función toma la URL del script, un selector CSS para el elemento que debe activar la carga y un callback opcional para la inicialización. Cuando el elemento entra en la vista, el script se inyecta y el observador se desconecta.

// Load the Google Maps API when the map container scrolls into view
injectScriptOnIntersection(
    'https://maps.googleapis.com/maps/api/js?key=YOUR_KEY',
    '#map-container',
    () => initMap()
);

IntersectionObserver es compatible con todos los navegadores modernos (95,76% de cobertura global según Can I Use). No se necesita polyfill.

En la interacción: cargar cuando el usuario interactúa

El método más efectivo es cargar un script solo cuando el visitante realmente interactúa con el elemento que lo necesita. Un widget de chat no necesita cargarse hasta que alguien haga clic en el botón de chat. Una biblioteca de validación de formularios no necesita cargarse hasta que el usuario haga foco en un campo del formulario.

function injectScriptOnInteraction(scriptUrl, elementSelector, eventTypes, callback) {
    const element = document.querySelector(elementSelector);
    const handler = () => {
        eventTypes.forEach(type => element.removeEventListener(type, handler));
        injectScript(scriptUrl, callback);
    };
    eventTypes.forEach(type => {
        element.addEventListener(type, handler);
    });
}

Esta función escucha los eventos especificados en el elemento objetivo. En el primer evento, elimina todos los listeners e inyecta el script. La ventaja: si el visitante nunca interactúa con el elemento, el script nunca se carga.

// Load chat widget script when the chat button is clicked or hovered
injectScriptOnInteraction(
    'chat-widget.js',
    '#chat-button',
    ['click', 'mouseover', 'touchstart'],
    () => initChat()
);

Impacto en el mundo real

Este patrón funciona para cualquier script que no se necesite durante la carga inicial de la página. Algunos casos de uso comunes:

  • Widgets de chat: Un widget de chat típico carga de 200 a 400 KB de JavaScript. Cuando Postmark difirió su widget de Intercom para cargarse al hacer clic en lugar de cargarse de forma anticipada, su Time to Interactive bajó de 7,7 segundos a 3,7 segundos.
  • Incrustaciones de video: Una incrustación de YouTube carga más de 1 MB de datos. Muestra una miniatura con un botón de reproducción y carga la incrustación al hacer clic.
  • Integraciones de mapas: Google Maps carga cientos de kilobytes de JavaScript. Usa IntersectionObserver para cargarlo cuando el contenedor del mapa entre en la vista.
  • Analítica y seguimiento: Los scripts de analítica pueden esperar hasta después de la primera interacción del usuario. Nadie se ha decepcionado nunca porque su herramienta de mapa de calor comenzara a grabar 3 segundos después de la carga de la página.
  • Bibliotecas de formularios: Las bibliotecas de validación, selectores de fecha y editores de texto enriquecido pueden cargarse cuando el usuario haga foco en el formulario.

Cuándo no diferir

No todos los scripts deben diferirse. Si un script es responsable de renderizar contenido visible en la primera pantalla, diferirlo empeorará tu Largest Contentful Paint, no lo mejorará. Los scripts que inicializan la navegación del encabezado, renderizan la sección hero o configuran variantes críticas de pruebas A/B necesitan ejecutarse temprano.

La regla es simple: si el visitante verá o interactuará con lo que produce el script dentro del primer viewport, cárgalo normalmente. Si el script alimenta algo debajo del pliegue o detrás de una acción del usuario, difiérelo usando uno de los patrones anteriores.

Consejo: Para una descripción completa de todas las estrategias de carga de JavaScript, consulta 16 métodos para diferir o programar JavaScript.

Midiendo la mejora

El Web Almanac 2025 reporta un Total Blocking Time mediano en móvil de 1.916ms, un 58% más que en 2024. Gran parte de ese bloqueo proviene de JavaScript que no necesitaba ejecutarse durante la carga de la página. Al diferir scripts no críticos, los eliminas completamente de la ruta crítica.

Después de implementar la carga bajo demanda, verifica la mejora con Real User Monitoring. Revisa tus puntuaciones de INP y Total Blocking Time en datos de campo, no solo en Lighthouse. Las pruebas de laboratorio se ejecutan en máquinas rápidas con cachés vacías. Tus visitantes están en redes móviles con 15 pestañas de navegador abiertas. Ahí es donde se nota la diferencia.

About the author

Arjen Karel is a web performance consultant and the creator of CoreDash, a Real User Monitoring platform that tracks Core Web Vitals data across hundreds of sites. He also built the Core Web Vitals Visualizer Chrome extension. He has helped clients achieve passing Core Web Vitals scores on over 925,000 mobile URLs.

Find out what is actually slow.

I map your critical rendering path using real field data. You get a clear answer on what blocks LCP, what causes INP spikes, and where layout shifts originate.

Book a Deep Dive
Diferir scripts hasta que sean necesariosCore Web Vitals Diferir scripts hasta que sean necesarios