/** * @sc4rfurryx/proteusjs/perf * Performance guardrails and CWV-friendly patterns * * @version 2.0.0 * @author sc4rfurry * @license MIT */ export interface SpeculationOptions { prerender?: string[]; prefetch?: string[]; sameOriginOnly?: boolean; } export interface ContentVisibilityOptions { containIntrinsicSize?: string; } /** * Apply content-visibility for performance optimization */ export function contentVisibility( selector: string | Element, mode: 'auto' | 'hidden' = 'auto', opts: ContentVisibilityOptions = {} ): void { const elements = typeof selector === 'string' ? document.querySelectorAll(selector) : [selector]; const { containIntrinsicSize = '1000px 400px' } = opts; elements.forEach(element => { const el = element as HTMLElement; el.style.contentVisibility = mode; if (mode === 'auto') { el.style.containIntrinsicSize = containIntrinsicSize; } }); } /** * Set fetch priority for resources */ export function fetchPriority( selector: string | Element, priority: 'high' | 'low' | 'auto' ): void { const elements = typeof selector === 'string' ? document.querySelectorAll(selector) : [selector]; elements.forEach(element => { if (element instanceof HTMLImageElement || element instanceof HTMLLinkElement || element instanceof HTMLScriptElement) { (element as HTMLImageElement | HTMLLinkElement | HTMLScriptElement & { fetchPriority?: string }).fetchPriority = priority; } }); } /** * Set up speculation rules for prerendering and prefetching */ export function speculate(opts: SpeculationOptions): void { const { prerender = [], prefetch = [], sameOriginOnly = true } = opts; // Check for Speculation Rules API support if (!('supports' in HTMLScriptElement && HTMLScriptElement.supports('speculationrules'))) { console.warn('Speculation Rules API not supported'); return; } const rules: any = {}; if (prerender.length > 0) { rules.prerender = prerender.map(url => { const rule: any = { where: { href_matches: url } }; if (sameOriginOnly) { rule.where.href_matches = new URL(url, window.location.origin).href; } return rule; }); } if (prefetch.length > 0) { rules.prefetch = prefetch.map(url => { const rule: any = { where: { href_matches: url } }; if (sameOriginOnly) { rule.where.href_matches = new URL(url, window.location.origin).href; } return rule; }); } if (Object.keys(rules).length === 0) return; // Create and inject speculation rules script const script = document.createElement('script'); script.type = 'speculationrules'; script.textContent = JSON.stringify(rules); document.head.appendChild(script); } /** * Yield to browser using scheduler.yield or postTask when available */ export async function yieldToBrowser(): Promise { // Use scheduler.yield if available (Chrome 115+) if ('scheduler' in window && 'yield' in (window as any).scheduler) { return (window as any).scheduler.yield(); } // Use scheduler.postTask if available if ('scheduler' in window && 'postTask' in (window as any).scheduler) { return new Promise(resolve => { (window as any).scheduler.postTask(resolve, { priority: 'user-blocking' }); }); } // Fallback to setTimeout return new Promise(resolve => { setTimeout(resolve, 0); }); } /** * Optimize images with loading and decoding hints */ export function optimizeImages(selector: string | Element = 'img'): void { const images = typeof selector === 'string' ? document.querySelectorAll(selector) : [selector]; images.forEach(img => { if (!(img instanceof HTMLImageElement)) return; // Set loading attribute if not already set if (!img.hasAttribute('loading')) { const rect = img.getBoundingClientRect(); const isAboveFold = rect.top < window.innerHeight; img.loading = isAboveFold ? 'eager' : 'lazy'; } // Set decoding hint if (!img.hasAttribute('decoding')) { img.decoding = 'async'; } // Set fetch priority for above-fold images if (!img.hasAttribute('fetchpriority')) { const rect = img.getBoundingClientRect(); const isAboveFold = rect.top < window.innerHeight; if (isAboveFold) { (img as any).fetchPriority = 'high'; } } }); } /** * Preload critical resources */ export function preloadCritical(resources: Array<{ href: string; as: string; type?: string }>): void { resources.forEach(({ href, as, type }) => { // Check if already preloaded const existing = document.querySelector(`link[rel="preload"][href="${href}"]`); if (existing) return; const link = document.createElement('link'); link.rel = 'preload'; link.href = href; link.as = as; if (type) { link.type = type; } document.head.appendChild(link); }); } /** * Measure and report Core Web Vitals */ export function measureCWV(): Promise<{ lcp?: number; fid?: number; cls?: number }> { return new Promise(resolve => { const metrics: { lcp?: number; fid?: number; cls?: number } = {}; let metricsCount = 0; const totalMetrics = 3; const checkComplete = () => { metricsCount++; if (metricsCount >= totalMetrics) { resolve(metrics); } }; // LCP (Largest Contentful Paint) if ('PerformanceObserver' in window) { try { const lcpObserver = new PerformanceObserver(list => { const entries = list.getEntries(); const lastEntry = entries[entries.length - 1] as any; metrics.lcp = lastEntry.startTime; }); lcpObserver.observe({ entryTypes: ['largest-contentful-paint'] }); // Stop observing after 10 seconds setTimeout(() => { lcpObserver.disconnect(); checkComplete(); }, 10000); } catch { checkComplete(); } // FID (First Input Delay) try { const fidObserver = new PerformanceObserver(list => { const entries = list.getEntries(); entries.forEach((entry: any) => { metrics.fid = entry.processingStart - entry.startTime; }); fidObserver.disconnect(); checkComplete(); }); fidObserver.observe({ entryTypes: ['first-input'] }); // If no input after 10 seconds, consider FID as 0 setTimeout(() => { if (metrics.fid === undefined) { metrics.fid = 0; fidObserver.disconnect(); checkComplete(); } }, 10000); } catch { checkComplete(); } // CLS (Cumulative Layout Shift) try { let clsValue = 0; const clsObserver = new PerformanceObserver(list => { list.getEntries().forEach((entry: any) => { if (!entry.hadRecentInput) { clsValue += entry.value; } }); metrics.cls = clsValue; }); clsObserver.observe({ entryTypes: ['layout-shift'] }); // Stop observing after 10 seconds setTimeout(() => { clsObserver.disconnect(); checkComplete(); }, 10000); } catch { checkComplete(); } } else { // Fallback if PerformanceObserver is not supported setTimeout(() => resolve(metrics), 100); } }); } // Export boost object to match usage examples in upgrade spec export const boost = { contentVisibility, fetchPriority, speculate, yieldToBrowser, optimizeImages, preloadCritical, measureCWV }; // Export all functions as named exports and default object export default { contentVisibility, fetchPriority, speculate, yieldToBrowser, optimizeImages, preloadCritical, measureCWV, boost };