The Complete Guide to Lazy Loading reCAPTCHA for Maximum Performance
While reCAPTCHA is essential for security, its impact on page speed can be significant—especially for Core Web Vitals like Largest Contentful Paint (LCP). Esta guía completa le muestra cómo implementar la carga diferida para reCAPTCHA sin comprometer la seguridad ni la experiencia del usuario.
Why Lazy Load reCAPTCHA?
The Performance Problem:
reCAPTCHA v2/v3 scripts: ~100-300KB additional page weight
Múltiples solicitudes de red: Scripts, estilos y recursos
Potencial de bloqueo de renderizado: Puede retrasar LCP entre 1 y 3 segundos
Impacto móvil: Más grave en conexiones más lentas
The Solution:
Load reCAPTCHA only when needed—typically when a user begins interacting with a form. Esto puede mejorar el LCP entre un 15 % y un 40 % en páginas con muchos formularios.
Implementation Approaches
Method 1: Basic Focus-Based Lazy Load (Recommended)
Load reCAPTCHA when user interacts with any form field.
// reCAPTCHA Lazy Loader - Basic Version let reCaptchaLoaded = false; const formFields = document.querySelectorAll('input, textarea, select'); formFields.forEach(field => { field.addEventListener('focus', loadReCaptchaOnDemand); field.addEventListener('click', loadReCaptchaOnDemand); }); function loadReCaptchaOnDemand() { if (!reCaptchaLoaded) {// Cargar script reCAPTCHA const script = document.createElement('script'); script.src = 'https://www.google.com/recaptcha/api.js'; script.async = true; script.defer = true; document.head.appendChild(script);// Marcar como cargado reCaptchaLoaded = true;// Limpiar detectores de eventos (opcional) formFields.forEach(field => { field.removeEventListener('focus', loadReCaptchaOnDemand); field.removeEventListener('click', loadReCaptchaOnDemand); }); console.log('reCAPTCHA loaded on demand');}}
Method 2: Advanced Scroll-Based Lazy Load
Load reCAPTCHA when form becomes visible in viewport.
// Intersection Observer for Viewport Detection let reCaptchaLoaded = false; const observerOptions = { root: null, rootMargin: '100px', // Load 100px before entering viewport threshold: 0.1};const observer = new IntersectionObserver((entries) => { entries.forEach(entry => { if (entry.isIntersecting && !reCaptchaLoaded) { loadReCaptcha(); observer.unobserve(entry.target);} }); }, observerOptions);// Observe el contenedor de su formularioconst formContainer = document.getElementById('contact-form'); if (formContainer) { observer.observe(formContainer);}function loadReCaptcha() { const script = document.createElement('script'); script.src = 'https://www.google.com/recaptcha/api.js'; script.async = true; script.defer = true; script.onload = () => { reCaptchaLoaded = true; console.log('reCAPTCHA loaded via IntersectionObserver');}; document.head.appendChild(script); }
Method 3: Hybrid Approach (Focus + Scroll)
Best of both worlds—loads when form is visible OR when user interacts.
// Hybrid Lazy Loader class ReCaptchaLazyLoader { constructor() { this.loaded = false; this.observed = false; this.form = document.querySelector('form'); this.init();} init() { if (!this.form) return;// Método A: observar la visibilidad del formulario this.setupIntersectionObserver();// Método B: Escuche las interacciones del formulario this.setupInteractionListeners();// Alternativa: cargar después de 5 segundos si no hay interacción this.setupFallback();} setupIntersectionObserver() { if ('IntersectionObserver' in window) { const observer = new IntersectionObserver((entries) => { if (entries[0].isIntersecting) { this.load(); observer.unobserve(this.form);} }, { threshold: 0.1 }); observer.observe(this.form); this.observed = true;} } setupInteractionListeners() { const fields = this.form.querySelectorAll('input, textarea, select'); fields.forEach(field => { field.addEventListener('focus', () => this.load()); field.addEventListener('click', () => this.load()); });} setupFallback() { setTimeout(() => { if (!this.loaded && this.form.getBoundingClientRect().top < window.innerHeight) { this.load();} }, 5000);} load() { if (this.loaded) return; const script = document.createElement('script'); script.src = 'https://www.google.com/recaptcha/api.js'; script.async = true; script.defer = true; script.onload = () => { this.loaded = true; this.onLoadCallback();}; document.head.appendChild(script);} onLoadCallback() {// Inicializa tu reCAPTCHA aquí console.log('reCAPTCHA ready for initialization');// Ejemplo: grecaptcha.ready(() => { ... });} }// Inicializar cuando DOM esté listodocument.addEventListener('DOMContentLoaded', () => { new ReCaptchaLazyLoader(); });
reCAPTCHA v3 vs v2 Implementation
For reCAPTCHA v3 (Invisible):
// Lazy load v3 with token generation on form submit async function loadAndExecuteReCaptchaV3(action = 'submit') {// Cargar script si no está cargado if (typeof grecaptcha === 'undefined') { await new Promise((resolve) => { const script = document.createElement('script'); script.src = 'https://www.google.com/recaptcha/api.js?render=YOUR_SITE_KEY'; script.onload = resolve; document.head.appendChild(script); });} // Ejecutar reCAPTCHA return new Promise((resolve) => { grecaptcha.ready(() => { grecaptcha.execute('YOUR_SITE_KEY', { action: action }) .then(token => resolve(token)); }); });} // Uso en el envío de formulariosdocument.getElementById('myForm').addEventListener('submit', async (e) => { e.preventDefault(); const token = await loadAndExecuteReCaptchaV3();// Agregar token al formulario y enviar const input = document.createElement('input'); input.type = 'hidden'; input.name = 'g-recaptcha-response'; input.value = token; e.target.appendChild(input);// Continuar con el envío del formulario e.target.submit(); });
For reCAPTCHA v2 (Checkbox):
// Lazy load v2 with checkbox rendering function loadAndRenderReCaptchaV2() { if (typeof grecaptcha === 'undefined') { const script = document.createElement('script'); script.src = 'https://www.google.com/recaptcha/api.js'; script.onload = () => renderReCaptcha(); document.head.appendChild(script); } else { renderReCaptcha();} }function renderReCaptcha() { const container = document.getElementById('recaptcha-container'); if (container && !container.querySelector('.g-recaptcha')) { grecaptcha.render(container, { sitekey: 'YOUR_SITE_KEY', theme: 'light', // or 'dark' size: 'normal' // or 'compact' });} }// Activar en la interacción del formulariodocument.getElementById('message').addEventListener('focus', loadAndRenderReCaptchaV2);
Performance Optimizations
1.Preconnect for Faster Loading
Add to your HTML :
<link rel="preconnect" href="https://www.google.com"> <link rel="preconnect" href="https://www.gstatic.com" crossorigin>
2.Resource Hints
<link rel="dns-prefetch" href="//www.google.com"> <link rel="preload" as="script" href="https://www.google.com/recaptcha/api.js" onload="this.onload=null;this.rel='prefetch'">
3.Adaptive Loading Based on Connection
function shouldLoadReCaptchaEarly() { const connection = navigator.connection || navigator.mozConnection || navigator.webkitConnection; if (connection) {// Carga temprana en conexiones rápidas if (connection.effectiveType === '4g' || connection.saveData === false || (connection.downlink && connection.downlink > 3)) { // >3 Mbps return true;} }// Carga predeterminada predeterminada return false;}if (shouldLoadReCaptchaEarly()) {// Carga reCAPTCHA inmediatamente en conexiones rápidas loadReCaptcha(); } else {// Utilice carga diferida en conexiones más lentas setupLazyLoading(); }
Error Handling & Edge Cases
1.Network Error Handling
function loadReCaptchaWithRetry(retries = 3, delay = 1000) { return new Promise((resolve, reject) => { const script = document.createElement('script'); script.src = 'https://www.google.com/recaptcha/api.js'; script.async = true; script.onload = resolve; script.onerror = () => { if (retries > 0) { setTimeout(() => { loadReCaptchaWithRetry(retries - 1, delay * 2) .then(resolve) .catch(reject); }, delay); } else { reject(new Error('Failed to load reCAPTCHA after multiple attempts'));}}; document.head.appendChild(script); }); }
2.Form Submission Before reCAPTCHA Loads
document.getElementById('contact-form').addEventListener('submit', async (e) => { e.preventDefault();// Mostrar estado de carga const submitBtn = e.target.querySelector('button[type="submit"]'); const originalText = submitBtn.textContent; submitBtn.textContent = 'Verifying...'; submitBtn.disabled = true;probar {// Asegúrese de que reCAPTCHA esté cargado if (typeof grecaptcha === 'undefined') { await loadReCaptchaWithRetry(); await new Promise(resolve => grecaptcha.ready(resolve));}// Ejecutar reCAPTCHA const token = await grecaptcha.execute('YOUR_SITE_KEY', { action: 'submit' });// Continuar con el envío del formulario// ... tu lógica de envío } catch (error) { console.error('reCAPTCHA error:', error);// Alternativa: utilizar validación alternativa o mostrar error alert('Security verification failed. Por favor, inténtelo de nuevo.'); submitBtn.textContent = originalText; submitBtn.disabled = false;}});
Testing & Measurement
Performance Testing Script:
// Measure reCAPTCHA impact on LCP const observer = new PerformanceObserver((list) => { for (const entry of list.getEntries()) { if (entry.name === 'reCAPTCHA-load') { console.log(`reCAPTCHA loaded in ${entry.duration}ms`); console.log(`LCP before reCAPTCHA: ${window.lcpBeforeReCaptcha}`);} }}); observer.observe({ entryTypes: ['measure'] });// Marcar el tiempo de LCP antes de reCAPTCHAwindow.lcpBeforeReCaptcha = performance.now();// Iniciar la medición cuando comience la carga diferidaperformance.mark('reCAPTCHA-start');// ... después de que se complete la cargaperformance.mark('reCAPTCHA-end'); performance.measure('reCAPTCHA-load', 'reCAPTCHA-start', 'reCAPTCHA-end');
Core Web Vitals Monitoring:
// Track CLS impact let clsBefore, clsAfter; function measureCLSImpact() { const observer = new PerformanceObserver((list) => { for (const entry of list.getEntries()) { if (entry.name === 'layout-shift' && entry.hadRecentInput === false) { if (!clsBefore) clsBefore = entry.value; else clsAfter = entry.value;} } }); observer.observe({ entryTypes: ['layout-shift'] }); }
Framework-Specific Implementations
React Component:
import { useEffect, useRef, useState } from 'react'; function LazyReCaptcha({ siteKey, onLoad }) { const [loaded, setLoaded] = useState(false); const formRef = useRef(null); useEffect(() => { const form = formRef.current; if (!form) return; const loadReCaptcha = () => { if (!loaded && typeof window.grecaptcha === 'undefined') { const script = document.createElement('script'); script.src = `https://www.google.com/recaptcha/api.js?render=${siteKey}`; script.async = true; script.onload = () => { setLoaded(true); onLoad?.();}; document.head.appendChild(script);}};// Agregar detectores de eventos a todas las entradas del formulario const inputs = form.querySelectorAll('input, textarea'); inputs.forEach(input => { input.addEventListener('focus', loadReCaptcha, { once: true }); }); return () => { inputs.forEach(input => { input.removeEventListener('focus', loadReCaptcha); });}; }, [siteKey, loaded, onLoad]); return <div ref={formRef}>{/* Your form content */}div>; }
Vue.js Directive:
// Vue directive for lazy loading reCAPTCHA export const lazyRecaptcha = { mounted(el, binding) { const { siteKey, onLoad } = binding.value; let loaded = false; const loadScript = () => { if (!loaded && typeof grecaptcha === 'undefined') { const script = document.createElement('script'); script.src = `https://www.google.com/recaptcha/api.js?render=${siteKey}`; script.async = true; script.onload = () => { loaded = true; onLoad?.();}; document.head.appendChild(script);}};// Agregar detectores de eventos a todos los elementos interactivos const elements = el.querySelectorAll('input, textarea, select, button'); elements.forEach(element => { element.addEventListener('focus', loadScript, { once: true }); element.addEventListener('click', loadScript, { once: true }); });}};
Best Practices Summary
Do:
✅ Test on slow 3G connections to ensure usability
✅ Proporcione comentarios visuales mientras se carga reCAPTCHA
✅ Implemente mecanismos de respaldo para fallas de la red
✅ Monitorear Core Web Vitals antes/después de la implementación
✅ Utilizar activadores apropiados (enfoque, desplazamiento o híbrido)
✅ Considere la carga basada en la conexión para redes rápidas
Don't:
❌ Block form submission if reCAPTCHA fails to load
❌ Olvídate de probar la accesibilidad (lectores de pantalla, navegación por teclado)
❌ Ignora el rendimiento móvil - prueba en dispositivos reales
❌ Carga reCAPTCHA en cada página , solo cuando sea necesario
❌ Sacrifica la seguridad por rendimiento: encuentra el equilibrio