Aprenda técnicas avançadas de otimização para criar sites rápidos, eficientes e com excelente experiência do usuário.
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.
<!-- 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>
<!-- 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>
<!-- 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>
Faça uma auditoria completa de performance:
<!-- 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 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>
<!-- 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>
Crie uma galeria de imagens com máxima performance:
<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>
<!-- 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>
<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>
<!-- 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>
Transforme um site em PWA otimizada:
<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>
<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>
<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>
Implemente um sistema completo de monitoramento:
LCP, FID, CLS - métricas essenciais de experiência do usuário.
Imagens responsivas, lazy loading e formatos modernos.
Resource hints, code splitting e cache strategies.
RUM, Performance Budget e métricas em tempo real.