AULA 11

⚡ Performance e Otimização

Aprenda técnicas avançadas de otimização para criar sites rápidos, eficientes e com excelente experiência do usuário.

⏱️ 65 minutos
🎯 Avançado
📝 4 Exercícios

🎯 Objetivos de Aprendizagem

📊 1. Core Web Vitals

🎯 O que são Core Web Vitals?

Core Web Vitals são métricas essenciais do Google que medem a experiência do usuário em termos de carregamento, interatividade e estabilidade visual.

🚀 LCP - Largest Contentful Paint

<!-- Otimizando LCP - elemento principal deve carregar rápido -->

<!-- ❌ RUIM: Imagem grande sem otimização -->
<img src="hero-image.jpg" alt="Hero image">

<!-- ✅ BOM: Imagem otimizada com preload -->
<head>
    <link rel="preload" as="image" href="hero-optimized.webp">
</head>

<picture>
    <source srcset="hero-optimized.webp" type="image/webp">
    <source srcset="hero-optimized.jpg" type="image/jpeg">
    <img src="hero-optimized.jpg" 
         alt="Imagem principal do site"
         width="1200" 
         height="600"
         loading="eager">
</picture>

<!-- Preload para fontes críticas -->
<link rel="preload" 
      href="fonts/primary-font.woff2" 
      as="font" 
      type="font/woff2" 
      crossorigin>

<!-- Evitar mudanças de layout durante carregamento -->
<div class="hero-container" style="aspect-ratio: 2/1;">
    <img src="hero.jpg" alt="Hero" loading="eager">
</div>

⚡ FID - First Input Delay

<!-- Otimizando FID - reduzir bloqueio de JavaScript -->

<!-- ❌ RUIM: Scripts bloqueantes no head -->
<head>
    <script src="heavy-library.js"></script>
    <script src="analytics.js"></script>
</head>

<!-- ✅ BOM: Scripts assíncronos e não-críticos no final -->
<head>
    <!-- Apenas CSS crítico no head -->
    <style>
        /* CSS crítico inline para primeira renderização */
        .hero { 
            background: #007bff; 
            height: 60vh; 
        }
        .nav { 
            background: #fff; 
            position: fixed; 
            top: 0; 
        }
    </style>
</head>

<body>
    <!-- Conteúdo da página -->
    
    <!-- Scripts no final do body -->
    <script async src="analytics.js"></script>
    <script defer src="heavy-library.js"></script>
    
    <!-- CSS não-crítico carregado de forma assíncrona -->
    <script>
        // Carregar CSS não-crítico após load
        window.addEventListener('load', function() {
            const link = document.createElement('link');
            link.rel = 'stylesheet';
            link.href = 'non-critical.css';
            document.head.appendChild(link);
        });
    </script>
</body>

📐 CLS - Cumulative Layout Shift

<!-- Prevenindo CLS - reservar espaço para elementos -->

<!-- ❌ RUIM: Sem dimensões especificadas -->
<img src="product.jpg" alt="Produto">
<div id="ad-banner"></div>

<!-- ✅ BOM: Dimensões especificadas -->
<img src="product.jpg" 
     alt="Produto" 
     width="400" 
     height="300"
     style="aspect-ratio: 4/3;">

<!-- Placeholder para conteúdo dinâmico -->
<div id="ad-banner" 
     style="width: 728px; height: 90px; background: #f0f0f0;">
    <!-- Conteúdo do banner será inserido aqui -->
</div>

<!-- Skeleton loading para prevenir shifts -->
<div class="comment-skeleton" aria-hidden="true">
    <div class="skeleton-avatar"></div>
    <div class="skeleton-content">
        <div class="skeleton-line"></div>
        <div class="skeleton-line short"></div>
    </div>
</div>

<!-- Fontes web com fallbacks para evitar FOIT/FOUT -->
<style>
    body {
        font-family: 'CustomFont', Arial, sans-serif;
        font-display: swap; /* Troca imediata quando fonte carrega */
    }
    
    .skeleton-avatar {
        width: 40px;
        height: 40px;
        background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
        background-size: 200% 100%;
        animation: loading 1.5s infinite;
        border-radius: 50%;
    }
    
    @keyframes loading {
        0% { background-position: 200% 0; }
        100% { background-position: -200% 0; }
    }
</style>

⚠️ Metas dos Core Web Vitals

  • LCP: ≤ 2.5 segundos
  • FID: ≤ 100 milissegundos
  • CLS: ≤ 0.1
  • Meta: 75% das visitas devem atingir esses valores

🏋️‍♂️ Exercício 1: Auditoria de Core Web Vitals

Faça uma auditoria completa de performance:

🖼️ 2. Otimização de Imagens

📱 Imagens Responsivas

<!-- Picture element para diferentes contextos -->
<picture>
    <!-- Imagem para desktop -->
    <source media="(min-width: 1024px)" 
            srcset="hero-desktop.webp 1200w,
                    hero-desktop-2x.webp 2400w"
            sizes="100vw"
            type="image/webp">
    
    <!-- Imagem para tablet -->
    <source media="(min-width: 768px)" 
            srcset="hero-tablet.webp 768w,
                    hero-tablet-2x.webp 1536w"
            sizes="100vw"
            type="image/webp">
    
    <!-- Imagem para mobile -->
    <source media="(max-width: 767px)" 
            srcset="hero-mobile.webp 400w,
                    hero-mobile-2x.webp 800w"
            sizes="100vw"
            type="image/webp">
    
    <!-- Fallback JPEG -->
    <img src="hero-desktop.jpg" 
         alt="Imagem principal do site"
         width="1200" 
         height="600"
         loading="eager">
</picture>

<!-- Srcset simples para diferentes densidades -->
<img src="product.jpg"
     srcset="product-1x.jpg 1x,
             product-2x.jpg 2x,
             product-3x.jpg 3x"
     alt="Produto em destaque"
     width="300"
     height="200">

⚡ Lazy Loading

<!-- Lazy loading nativo -->
<img src="image1.jpg" 
     alt="Imagem da galeria"
     loading="lazy"
     width="400" 
     height="300">

<!-- Primeira imagem sempre eager -->
<img src="hero.jpg" 
     alt="Imagem principal"
     loading="eager"
     width="1200" 
     height="600">

<!-- Lazy loading avançado com Intersection Observer -->
<img data-src="high-res-image.jpg" 
     src="placeholder-blur.jpg"
     alt="Imagem de alta resolução"
     class="lazy-load"
     width="800" 
     height="400">

<script>
// Implementação de lazy loading com blur-up
class LazyImageLoader {
    constructor() {
        this.imageObserver = null;
        this.init();
    }
    
    init() {
        if ('IntersectionObserver' in window) {
            this.imageObserver = new IntersectionObserver((entries, observer) => {
                entries.forEach(entry => {
                    if (entry.isIntersecting) {
                        this.loadImage(entry.target);
                        observer.unobserve(entry.target);
                    }
                });
            }, {
                rootMargin: '50px 0px' // Carregar 50px antes de aparecer
            });
            
            this.observeImages();
        } else {
            // Fallback para navegadores antigos
            this.loadAllImages();
        }
    }
    
    observeImages() {
        const lazyImages = document.querySelectorAll('.lazy-load');
        lazyImages.forEach(img => this.imageObserver.observe(img));
    }
    
    loadImage(img) {
        const src = img.dataset.src;
        if (!src) return;
        
        // Criar nova imagem para preload
        const imageLoader = new Image();
        imageLoader.onload = () => {
            img.src = src;
            img.classList.add('loaded');
        };
        imageLoader.src = src;
    }
    
    loadAllImages() {
        const lazyImages = document.querySelectorAll('.lazy-load');
        lazyImages.forEach(img => this.loadImage(img));
    }
}

// Inicializar lazy loading
new LazyImageLoader();
</script>

<style>
.lazy-load {
    filter: blur(5px);
    transition: filter 0.3s ease;
}

.lazy-load.loaded {
    filter: blur(0);
}
</style>

🎨 Formatos Modernos

<!-- Usando WebP com fallback -->
<picture>
    <source srcset="image.avif" type="image/avif">
    <source srcset="image.webp" type="image/webp">
    <img src="image.jpg" alt="Descrição da imagem">
</picture>

<!-- SVG otimizado para ícones -->
<svg width="24" height="24" viewBox="0 0 24 24" aria-hidden="true">
    <path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/>
</svg>

<!-- CSS Sprites para ícones pequenos -->
<div class="icon-star"></div>

<style>
.icon-star {
    width: 24px;
    height: 24px;
    background: url('icons-sprite.webp') -48px -24px;
    background-size: 200px 100px;
}

/* CSS para imagens de fundo otimizadas */
.hero-background {
    background-image: 
        image-set(
            url('hero.webp') 1x,
            url('hero-2x.webp') 2x
        );
    background-size: cover;
    background-position: center;
}

/* Suporte a WebP via CSS */
.webp .hero-background {
    background-image: url('hero.webp');
}

.no-webp .hero-background {
    background-image: url('hero.jpg');
}
</style>

🏋️‍♂️ Exercício 2: Galeria de Imagens Otimizada

Crie uma galeria de imagens com máxima performance:

🧠 3. Carregamento Inteligente

🚀 Resource Hints

<head>
    <!-- DNS prefetch para domínios externos -->
    <link rel="dns-prefetch" href="//fonts.googleapis.com">
    <link rel="dns-prefetch" href="//www.google-analytics.com">
    <link rel="dns-prefetch" href="//cdnjs.cloudflare.com">
    
    <!-- Preconnect para recursos críticos -->
    <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
    
    <!-- Preload para recursos críticos -->
    <link rel="preload" href="critical.css" as="style">
    <link rel="preload" href="hero.webp" as="image">
    <link rel="preload" href="main-font.woff2" as="font" type="font/woff2" crossorigin>
    
    <!-- Prefetch para recursos que serão usados depois -->
    <link rel="prefetch" href="next-page.html">
    <link rel="prefetch" href="secondary.css">
    
    <!-- Modulepreload para ES modules -->
    <link rel="modulepreload" href="app.js">
</head>

📦 Code Splitting

<!-- Carregamento modular de JavaScript -->
<script type="module">
    // Carregamento dinâmico de módulos
    async function loadFeature(featureName) {
        try {
            const module = await import(`./features/${featureName}.js`);
            return module.default;
        } catch (error) {
            console.error(`Erro ao carregar ${featureName}:`, error);
        }
    }
    
    // Carregar funcionalidades sob demanda
    document.addEventListener('DOMContentLoaded', async () => {
        // Carregar funcionalidade crítica imediatamente
        const core = await loadFeature('core');
        core.init();
        
        // Carregar outras funcionalidades quando necessário
        const lazyFeatures = ['gallery', 'comments', 'sharing'];
        
        const observer = new IntersectionObserver((entries) => {
            entries.forEach(async (entry) => {
                if (entry.isIntersecting) {
                    const feature = entry.target.dataset.feature;
                    if (lazyFeatures.includes(feature)) {
                        const module = await loadFeature(feature);
                        module.init(entry.target);
                        observer.unobserve(entry.target);
                    }
                }
            });
        });
        
        document.querySelectorAll('[data-feature]').forEach(el => {
            observer.observe(el);
        });
    });
</script>

<!-- Elementos que ativam carregamento de funcionalidades -->
<div class="gallery" data-feature="gallery">
    <!-- Galeria será inicializada quando visível -->
</div>

<div class="comments-section" data-feature="comments">
    <!-- Sistema de comentários carregado sob demanda -->
</div>

⚡ Critical CSS

<head>
    <!-- CSS crítico inline -->
    <style>
        /* CSS para above-the-fold content */
        * { box-sizing: border-box; margin: 0; padding: 0; }
        
        body { 
            font-family: -apple-system, BlinkMacSystemFont, sans-serif;
            line-height: 1.6;
            color: #333;
        }
        
        .header {
            background: #fff;
            box-shadow: 0 2px 4px rgba(0,0,0,0.1);
            position: fixed;
            top: 0;
            width: 100%;
            z-index: 1000;
        }
        
        .hero {
            height: 100vh;
            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
            display: flex;
            align-items: center;
            justify-content: center;
            color: white;
            text-align: center;
        }
        
        .hero h1 {
            font-size: clamp(2rem, 5vw, 4rem);
            margin-bottom: 1rem;
        }
        
        /* CSS crítico para layout */
        .container { max-width: 1200px; margin: 0 auto; padding: 0 1rem; }
        .btn { padding: 0.75rem 1.5rem; border: none; border-radius: 4px; cursor: pointer; }
        .btn-primary { background: #007bff; color: white; }
    </style>
    
    <!-- Carregar CSS completo de forma assíncrona -->
    <script>
        // Função para carregar CSS de forma assíncrona
        function loadCSS(href, media = 'all') {
            const link = document.createElement('link');
            link.rel = 'stylesheet';
            link.href = href;
            link.media = 'print'; // Carregar como print primeiro
            link.onload = () => link.media = media; // Mudar para all quando carregado
            document.head.appendChild(link);
        }
        
        // Carregar CSS não-crítico após DOMContentLoaded
        document.addEventListener('DOMContentLoaded', () => {
            loadCSS('styles/complete.css');
            loadCSS('styles/components.css');
        });
        
        // Fallback caso JavaScript esteja desabilitado
    </script>
    <noscript>
        <link rel="stylesheet" href="styles/complete.css">
    </noscript>
</head>

🔄 Service Workers

<!-- Registrar Service Worker -->
<script>
    if ('serviceWorker' in navigator) {
        window.addEventListener('load', async () => {
            try {
                const registration = await navigator.serviceWorker.register('/sw.js');
                console.log('SW registrado:', registration.scope);
            } catch (error) {
                console.log('Falha no registro do SW:', error);
            }
        });
    }
</script>

<!-- sw.js - Service Worker para cache inteligente -->
<script>
// Cache strategy no Service Worker
const CACHE_NAME = 'site-v1';
const CRITICAL_ASSETS = [
    '/',
    '/styles/critical.css',
    '/js/core.js',
    '/images/hero.webp'
];

// Instalar SW e cachear recursos críticos
self.addEventListener('install', event => {
    event.waitUntil(
        caches.open(CACHE_NAME)
            .then(cache => cache.addAll(CRITICAL_ASSETS))
    );
});

// Estratégia de cache: Cache First para assets, Network First para HTML
self.addEventListener('fetch', event => {
    const { request } = event;
    
    // Estratégia para diferentes tipos de recursos
    if (request.destination === 'image') {
        // Cache First para imagens
        event.respondWith(cacheFirst(request));
    } else if (request.destination === 'document') {
        // Network First para HTML
        event.respondWith(networkFirst(request));
    } else {
        // Stale While Revalidate para outros recursos
        event.respondWith(staleWhileRevalidate(request));
    }
});

async function cacheFirst(request) {
    const cached = await caches.match(request);
    return cached || fetch(request);
}

async function networkFirst(request) {
    try {
        const response = await fetch(request);
        const cache = await caches.open(CACHE_NAME);
        cache.put(request, response.clone());
        return response;
    } catch {
        return caches.match(request);
    }
}

async function staleWhileRevalidate(request) {
    const cached = await caches.match(request);
    const fetchPromise = fetch(request).then(response => {
        const cache = caches.open(CACHE_NAME);
        cache.then(c => c.put(request, response.clone()));
        return response;
    });
    
    return cached || fetchPromise;
}
</script>

🏋️‍♂️ Exercício 3: Progressive Web App

Transforme um site em PWA otimizada:

📈 4. Monitoramento e Medição

📊 Performance Observer API

<script>
// Monitoramento abrangente de performance
class PerformanceMonitor {
    constructor() {
        this.metrics = new Map();
        this.init();
    }
    
    init() {
        // Observar Core Web Vitals
        this.observeLCP();
        this.observeFID();
        this.observeCLS();
        
        // Observar outras métricas
        this.observeNavigation();
        this.observeResources();
        
        // Enviar métricas quando página estiver sendo fechada
        window.addEventListener('beforeunload', () => {
            this.sendMetrics();
        });
    }
    
    observeLCP() {
        new PerformanceObserver((entryList) => {
            const entries = entryList.getEntries();
            const lastEntry = entries[entries.length - 1];
            
            this.metrics.set('LCP', {
                value: lastEntry.startTime,
                rating: lastEntry.startTime < 2500 ? 'good' : 
                       lastEntry.startTime < 4000 ? 'needs-improvement' : 'poor'
            });
        }).observe({ entryTypes: ['largest-contentful-paint'] });
    }
    
    observeFID() {
        new PerformanceObserver((entryList) => {
            const entries = entryList.getEntries();
            entries.forEach(entry => {
                this.metrics.set('FID', {
                    value: entry.processingStart - entry.startTime,
                    rating: entry.processingStart - entry.startTime < 100 ? 'good' : 
                           entry.processingStart - entry.startTime < 300 ? 'needs-improvement' : 'poor'
                });
            });
        }).observe({ entryTypes: ['first-input'] });
    }
    
    observeCLS() {
        let clsValue = 0;
        new PerformanceObserver((entryList) => {
            const entries = entryList.getEntries();
            entries.forEach(entry => {
                if (!entry.hadRecentInput) {
                    clsValue += entry.value;
                }
            });
            
            this.metrics.set('CLS', {
                value: clsValue,
                rating: clsValue < 0.1 ? 'good' : 
                       clsValue < 0.25 ? 'needs-improvement' : 'poor'
            });
        }).observe({ entryTypes: ['layout-shift'] });
    }
    
    observeNavigation() {
        new PerformanceObserver((entryList) => {
            const entries = entryList.getEntries();
            entries.forEach(entry => {
                this.metrics.set('TTFB', {
                    value: entry.responseStart - entry.requestStart,
                    rating: entry.responseStart - entry.requestStart < 200 ? 'good' : 
                           entry.responseStart - entry.requestStart < 500 ? 'needs-improvement' : 'poor'
                });
                
                this.metrics.set('DOMContentLoaded', {
                    value: entry.domContentLoadedEventEnd - entry.domContentLoadedEventStart
                });
                
                this.metrics.set('Load', {
                    value: entry.loadEventEnd - entry.loadEventStart
                });
            });
        }).observe({ entryTypes: ['navigation'] });
    }
    
    observeResources() {
        new PerformanceObserver((entryList) => {
            const entries = entryList.getEntries();
            const resourceMetrics = {};
            
            entries.forEach(entry => {
                const type = entry.initiatorType;
                if (!resourceMetrics[type]) {
                    resourceMetrics[type] = { count: 0, totalSize: 0, totalTime: 0 };
                }
                
                resourceMetrics[type].count++;
                resourceMetrics[type].totalSize += entry.transferSize || 0;
                resourceMetrics[type].totalTime += entry.duration;
            });
            
            this.metrics.set('Resources', resourceMetrics);
        }).observe({ entryTypes: ['resource'] });
    }
    
    // Métricas customizadas
    measureCustomMetric(name, startTime, endTime = performance.now()) {
        this.metrics.set(name, {
            value: endTime - startTime,
            custom: true
        });
    }
    
    startMeasure(name) {
        performance.mark(`${name}-start`);
    }
    
    endMeasure(name) {
        performance.mark(`${name}-end`);
        performance.measure(name, `${name}-start`, `${name}-end`);
        
        const measure = performance.getEntriesByName(name, 'measure')[0];
        this.metrics.set(name, {
            value: measure.duration,
            custom: true
        });
    }
    
    sendMetrics() {
        const metricsData = Object.fromEntries(this.metrics);
        
        // Usar sendBeacon para envio confiável
        if (navigator.sendBeacon) {
            navigator.sendBeacon('/api/metrics', JSON.stringify({
                url: window.location.href,
                userAgent: navigator.userAgent,
                metrics: metricsData,
                timestamp: Date.now()
            }));
        } else {
            // Fallback para fetch
            fetch('/api/metrics', {
                method: 'POST',
                body: JSON.stringify(metricsData),
                headers: { 'Content-Type': 'application/json' }
            }).catch(() => {
                // Salvar localmente se envio falhar
                localStorage.setItem('pendingMetrics', JSON.stringify(metricsData));
            });
        }
    }
    
    getMetrics() {
        return Object.fromEntries(this.metrics);
    }
}

// Inicializar monitor de performance
const perfMonitor = new PerformanceMonitor();

// Exemplo de uso de métricas customizadas
perfMonitor.startMeasure('image-gallery-load');
// ... código para carregar galeria ...
perfMonitor.endMeasure('image-gallery-load');

// Relatório em tempo real
console.log('📊 Métricas de Performance:', perfMonitor.getMetrics());
</script>

🎯 Real User Monitoring (RUM)

<script>
// Sistema de RUM personalizado
class RealUserMonitoring {
    constructor(config = {}) {
        this.config = {
            endpoint: '/api/rum',
            sampleRate: 0.1, // 10% dos usuários
            ...config
        };
        
        this.sessionData = {
            sessionId: this.generateSessionId(),
            userId: this.getUserId(),
            startTime: Date.now(),
            pageViews: 0,
            interactions: 0,
            errors: []
        };
        
        if (Math.random() < this.config.sampleRate) {
            this.init();
        }
    }
    
    init() {
        this.trackPageView();
        this.trackInteractions();
        this.trackErrors();
        this.trackPerformance();
        this.trackConnectivity();
    }
    
    trackPageView() {
        this.sessionData.pageViews++;
        
        this.send('pageview', {
            url: window.location.href,
            referrer: document.referrer,
            title: document.title,
            viewport: {
                width: window.innerWidth,
                height: window.innerHeight
            },
            device: this.getDeviceInfo()
        });
    }
    
    trackInteractions() {
        ['click', 'scroll', 'keydown'].forEach(eventType => {
            document.addEventListener(eventType, (event) => {
                this.sessionData.interactions++;
                
                if (eventType === 'click') {
                    this.send('interaction', {
                        type: 'click',
                        element: event.target.tagName,
                        classes: event.target.className,
                        text: event.target.textContent?.substring(0, 50)
                    });
                }
            }, { passive: true });
        });
    }
    
    trackErrors() {
        window.addEventListener('error', (event) => {
            this.sessionData.errors.push({
                message: event.message,
                filename: event.filename,
                line: event.lineno,
                column: event.colno,
                timestamp: Date.now()
            });
            
            this.send('error', {
                type: 'javascript',
                message: event.message,
                stack: event.error?.stack,
                url: event.filename,
                line: event.lineno
            });
        });
        
        window.addEventListener('unhandledrejection', (event) => {
            this.send('error', {
                type: 'promise-rejection',
                reason: event.reason?.toString(),
                stack: event.reason?.stack
            });
        });
    }
    
    trackPerformance() {
        // Integrar com PerformanceMonitor
        if (window.perfMonitor) {
            setInterval(() => {
                const metrics = window.perfMonitor.getMetrics();
                this.send('performance', metrics);
            }, 30000); // A cada 30 segundos
        }
    }
    
    trackConnectivity() {
        if ('connection' in navigator) {
            const connection = navigator.connection;
            
            this.send('connectivity', {
                effectiveType: connection.effectiveType,
                downlink: connection.downlink,
                rtt: connection.rtt,
                saveData: connection.saveData
            });
            
            connection.addEventListener('change', () => {
                this.send('connectivity-change', {
                    effectiveType: connection.effectiveType,
                    downlink: connection.downlink,
                    rtt: connection.rtt
                });
            });
        }
    }
    
    send(eventType, data) {
        const payload = {
            eventType,
            data,
            sessionId: this.sessionData.sessionId,
            timestamp: Date.now(),
            url: window.location.href
        };
        
        if (navigator.sendBeacon) {
            navigator.sendBeacon(this.config.endpoint, JSON.stringify(payload));
        }
    }
    
    generateSessionId() {
        return 'sess_' + Math.random().toString(36).substr(2, 16);
    }
    
    getUserId() {
        let userId = localStorage.getItem('userId');
        if (!userId) {
            userId = 'user_' + Math.random().toString(36).substr(2, 16);
            localStorage.setItem('userId', userId);
        }
        return userId;
    }
    
    getDeviceInfo() {
        return {
            userAgent: navigator.userAgent,
            language: navigator.language,
            platform: navigator.platform,
            cookieEnabled: navigator.cookieEnabled,
            onLine: navigator.onLine,
            screen: {
                width: screen.width,
                height: screen.height,
                colorDepth: screen.colorDepth
            }
        };
    }
}

// Inicializar RUM
const rum = new RealUserMonitoring({
    endpoint: '/api/rum',
    sampleRate: 0.05 // 5% dos usuários
});
</script>

🔧 Performance Budget

<script>
// Sistema de Performance Budget
class PerformanceBudget {
    constructor() {
        this.budgets = {
            // Métricas de timing (em ms)
            LCP: 2500,
            FID: 100,
            TTFB: 200,
            
            // Métricas de recursos (em KB)
            totalSize: 2000,
            imageSize: 1000,
            scriptSize: 500,
            styleSize: 200,
            
            // Contadores
            requests: 50,
            
            // CLS sem unidade
            CLS: 0.1
        };
        
        this.violations = [];
        this.init();
    }
    
    init() {
        this.checkResourceBudget();
        this.checkTimingBudget();
        this.reportViolations();
    }
    
    checkResourceBudget() {
        const observer = new PerformanceObserver((list) => {
            const entries = list.getEntries();
            let totalSize = 0;
            let imageSize = 0;
            let scriptSize = 0;
            let styleSize = 0;
            let requests = entries.length;
            
            entries.forEach(entry => {
                const size = entry.transferSize || 0;
                totalSize += size;
                
                switch (entry.initiatorType) {
                    case 'img':
                    case 'image':
                        imageSize += size;
                        break;
                    case 'script':
                        scriptSize += size;
                        break;
                    case 'link':
                    case 'css':
                        styleSize += size;
                        break;
                }
            });
            
            // Converter para KB
            const metrics = {
                totalSize: Math.round(totalSize / 1024),
                imageSize: Math.round(imageSize / 1024),
                scriptSize: Math.round(scriptSize / 1024),
                styleSize: Math.round(styleSize / 1024),
                requests
            };
            
            // Verificar violações
            Object.entries(metrics).forEach(([metric, value]) => {
                if (this.budgets[metric] && value > this.budgets[metric]) {
                    this.addViolation(metric, value, this.budgets[metric]);
                }
            });
        });
        
        observer.observe({ entryTypes: ['resource'] });
    }
    
    checkTimingBudget() {
        // Verificar quando Core Web Vitals estiverem disponíveis
        if (window.perfMonitor) {
            setTimeout(() => {
                const metrics = window.perfMonitor.getMetrics();
                
                ['LCP', 'FID', 'CLS'].forEach(metric => {
                    if (metrics[metric] && metrics[metric].value > this.budgets[metric]) {
                        this.addViolation(metric, metrics[metric].value, this.budgets[metric]);
                    }
                });
            }, 5000);
        }
    }
    
    addViolation(metric, actual, budget) {
        const violation = {
            metric,
            actual,
            budget,
            excess: actual - budget,
            percentage: Math.round(((actual - budget) / budget) * 100),
            timestamp: Date.now()
        };
        
        this.violations.push(violation);
        
        // Log no console para desenvolvimento
        console.warn(
            `🚨 Performance Budget Violation: ${metric}`,
            `Actual: ${actual}, Budget: ${budget}, Excess: ${violation.excess} (+${violation.percentage}%)`
        );
    }
    
    reportViolations() {
        window.addEventListener('beforeunload', () => {
            if (this.violations.length > 0) {
                // Enviar violações para monitoramento
                navigator.sendBeacon('/api/budget-violations', JSON.stringify({
                    url: window.location.href,
                    violations: this.violations,
                    userAgent: navigator.userAgent
                }));
            }
        });
    }
    
    getReport() {
        return {
            budgets: this.budgets,
            violations: this.violations,
            status: this.violations.length === 0 ? 'PASS' : 'FAIL'
        };
    }
}

// Inicializar Performance Budget
const budgetMonitor = new PerformanceBudget();

// Mostrar relatório no console
setTimeout(() => {
    console.log('📊 Performance Budget Report:', budgetMonitor.getReport());
}, 10000);
</script>

🏋️‍♂️ Exercício 4: Sistema de Monitoramento Completo

Implemente um sistema completo de monitoramento:

📋 Resumo da Aula

📊 Core Web Vitals

LCP, FID, CLS - métricas essenciais de experiência do usuário.

🖼️ Otimização de Mídia

Imagens responsivas, lazy loading e formatos modernos.

🧠 Carregamento Inteligente

Resource hints, code splitting e cache strategies.

📈 Monitoramento

RUM, Performance Budget e métricas em tempo real.

✅ Checklist de Performance