/** * Performance Optimizer for Odoo Expertos * Adds lazy loading, caching, and optimizations without breaking existing functionality */ // 1. Lazy Load Images function setupLazyLoading() { // Create Intersection Observer for lazy loading images const imageObserver = new IntersectionObserver((entries, observer) => { entries.forEach(entry => { if (entry.isIntersecting) { const img = entry.target; // Load the actual image if (img.dataset.src) { img.src = img.dataset.src; img.removeAttribute('data-src'); } // Remove lazy loading class img.classList.remove('lazy-bg'); // Stop observing this image observer.unobserve(img); } }); }, { rootMargin: '50px' // Start loading 50px before entering viewport }); // Observe all images with data-src attribute document.querySelectorAll('img[data-src]').forEach(img => { imageObserver.observe(img); }); } // 2. Defer D3.js Loading function loadD3Async() { // Only load D3 when needed if (document.querySelector('.visualization, #network-visualization, .hero-visual')) { const script = document.createElement('script'); script.src = 'https://d3js.org/d3.v7.min.js'; script.async = true; script.onload = () => { // Initialize D3 visualizations after loading if (typeof initializeVisualizations === 'function') { initializeVisualizations(); } }; document.head.appendChild(script); } } // 3. Resource Hints for Critical Resources function addResourceHints() { const hints = [ { rel: 'preconnect', href: 'https://d3js.org' }, { rel: 'preconnect', href: 'https://images.pexels.com' }, { rel: 'dns-prefetch', href: 'https://fonts.googleapis.com' } ]; hints.forEach(hint => { const link = document.createElement('link'); link.rel = hint.rel; link.href = hint.href; if (hint.rel === 'preconnect') { link.crossOrigin = ''; } document.head.appendChild(link); }); } // 4. Cache API Responses using localStorage with TTL class CacheManager { constructor() { this.prefix = 'odoo_cache_'; this.defaultTTL = 3600000; // 1 hour in milliseconds } set(key, data, ttl = this.defaultTTL) { const cacheData = { data: data, timestamp: Date.now(), ttl: ttl }; try { localStorage.setItem(this.prefix + key, JSON.stringify(cacheData)); } catch (e) { // Handle quota exceeded this.clearOldCache(); try { localStorage.setItem(this.prefix + key, JSON.stringify(cacheData)); } catch (e) { console.warn('Cache storage full'); } } } get(key) { const item = localStorage.getItem(this.prefix + key); if (!item) return null; try { const cacheData = JSON.parse(item); const now = Date.now(); // Check if cache is expired if (now - cacheData.timestamp > cacheData.ttl) { localStorage.removeItem(this.prefix + key); return null; } return cacheData.data; } catch (e) { return null; } } clearOldCache() { const now = Date.now(); const keys = Object.keys(localStorage); keys.forEach(key => { if (key.startsWith(this.prefix)) { try { const item = JSON.parse(localStorage.getItem(key)); if (now - item.timestamp > item.ttl) { localStorage.removeItem(key); } } catch (e) { localStorage.removeItem(key); } } }); } } // 5. Optimize Component Loading function optimizeComponentLoading() { // Load header and footer asynchronously const loadComponent = (scriptSrc, callback) => { const script = document.createElement('script'); script.src = scriptSrc; script.async = true; script.onload = callback; document.head.appendChild(script); }; // Load components in parallel Promise.all([ new Promise(resolve => loadComponent('/components/header.js', resolve)), new Promise(resolve => loadComponent('/components/footer.js', resolve)) ]).then(() => { // Initialize components after loading if (typeof createHeader === 'function') { const headerPlaceholder = document.getElementById('header-placeholder'); if (headerPlaceholder) { headerPlaceholder.innerHTML = createHeader(); } } if (typeof createFooter === 'function') { const footerPlaceholder = document.getElementById('footer-placeholder'); if (footerPlaceholder) { footerPlaceholder.innerHTML = createFooter(); } } }); } // 6. Request Idle Callback for Non-Critical Tasks function deferNonCritical() { if ('requestIdleCallback' in window) { requestIdleCallback(() => { // Load cookie banner const cookieScript = document.createElement('script'); cookieScript.src = '/js/cookie-banner.js'; cookieScript.async = true; document.body.appendChild(cookieScript); // Preload next page resources preloadNextPage(); }); } else { // Fallback for browsers without requestIdleCallback setTimeout(() => { const cookieScript = document.createElement('script'); cookieScript.src = '/js/cookie-banner.js'; cookieScript.async = true; document.body.appendChild(cookieScript); preloadNextPage(); }, 2000); } } // 7. Preload likely next pages function preloadNextPage() { // Preload links that are likely to be clicked const links = document.querySelectorAll('a[href^="/"]'); const preloadedUrls = new Set(); links.forEach(link => { link.addEventListener('mouseenter', () => { const href = link.getAttribute('href'); if (!preloadedUrls.has(href)) { const prefetch = document.createElement('link'); prefetch.rel = 'prefetch'; prefetch.href = href; document.head.appendChild(prefetch); preloadedUrls.add(href); } }, { once: true }); }); } // 8. WebP Image Support Detection function checkWebPSupport(callback) { const webP = new Image(); webP.onload = webP.onerror = function () { callback(webP.height === 2); }; webP.src = 'data:image/webp;base64,UklGRjoAAABXRUJQVlA4IC4AAACyAgCdASoCAAIALmk0mk0iIiIiIgBoSygABc6WWgAA/veff/0PP8bA//LwYAAA'; } // 9. Initialize Performance Optimizations function initializeOptimizations() { // Add resource hints immediately addResourceHints(); // Setup lazy loading for images if ('IntersectionObserver' in window) { setupLazyLoading(); } // Load D3.js only when needed if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', loadD3Async); } else { loadD3Async(); } // Defer non-critical resources deferNonCritical(); // Check WebP support checkWebPSupport(supported => { if (supported) { document.documentElement.classList.add('webp'); } }); // Initialize cache manager window.cacheManager = new CacheManager(); // Clear old cache on load window.cacheManager.clearOldCache(); } // Start optimizations if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', initializeOptimizations); } else { initializeOptimizations(); } // Export for use in other scripts window.performanceOptimizer = { CacheManager, setupLazyLoading, loadD3Async, checkWebPSupport };