Medir é essencial para:
Mede quando o maior elemento de conteúdo é renderizado.
// Medir LCP
new PerformanceObserver((entryList) => {
const entries = entryList.getEntries();
const lastEntry = entries[entries.length - 1];
console.log('LCP:', lastEntry.renderTime || lastEntry.loadTime);
}).observe({ entryTypes: ['largest-contentful-paint'] });
Mede latência da primeira interação do usuário.
// Medir FID
new PerformanceObserver((entryList) => {
const entries = entryList.getEntries();
entries.forEach((entry) => {
console.log('FID:', entry.processingStart - entry.startTime);
});
}).observe({ entryTypes: ['first-input'] });
Mede instabilidade visual durante carregamento.
// Medir CLS
let clsValue = 0;
let clsEntries = [];
new PerformanceObserver((entryList) => {
for (const entry of entryList.getEntries()) {
if (!entry.hadRecentInput) {
clsValue += entry.value;
clsEntries.push(entry);
}
}
console.log('CLS:', clsValue);
}).observe({ entryTypes: ['layout-shift'] });
// Tempos de navegação
const perfData = performance.timing;
const pageLoadTime = perfData.loadEventEnd - perfData.navigationStart;
const domReadyTime = perfData.domContentLoadedEventEnd - perfData.navigationStart;
const connectTime = perfData.responseEnd - perfData.requestStart;
console.log('Page Load:', pageLoadTime);
console.log('DOM Ready:', domReadyTime);
console.log('Connect:', connectTime);
// Tempos de recursos
performance.getEntriesByType('resource').forEach((resource) => {
console.log({
name: resource.name,
duration: resource.duration,
size: resource.transferSize,
type: resource.initiatorType
});
});
// Marcações customizadas
performance.mark('inicio-processamento');
// ... código ...
performance.mark('fim-processamento');
performance.measure(
'tempo-processamento',
'inicio-processamento',
'fim-processamento'
);
const medida = performance.getEntriesByName('tempo-processamento')[0];
console.log('Tempo:', medida.duration);
npm install web-vitals
import { onLCP, onFID, onCLS } from 'web-vitals';
// LCP
onLCP((metric) => {
console.log('LCP:', metric.value);
// Enviar para analytics
sendToAnalytics('LCP', metric.value);
});
// FID
onFID((metric) => {
console.log('FID:', metric.value);
sendToAnalytics('FID', metric.value);
});
// CLS
onCLS((metric) => {
console.log('CLS:', metric.value);
sendToAnalytics('CLS', metric.value);
});
function sendToAnalytics(metricName, value) {
// Google Analytics
gtag('event', metricName, {
value: Math.round(value),
event_category: 'Web Vitals',
event_label: metricName,
non_interaction: true
});
// Ou enviar para seu backend
fetch('/api/metrics', {
method: 'POST',
body: JSON.stringify({
name: metricName,
value: value,
url: window.location.href
})
});
}
// Coletor de métricas
class PerformanceCollector {
constructor() {
this.metrics = {};
this.collect();
}
collect() {
// Core Web Vitals
this.collectWebVitals();
// Métricas customizadas
this.collectCustomMetrics();
// Enviar periodicamente
this.sendMetrics();
}
collectWebVitals() {
import('web-vitals').then(({ onLCP, onFID, onCLS }) => {
onLCP((metric) => this.metrics.lcp = metric.value);
onFID((metric) => this.metrics.fid = metric.value);
onCLS((metric) => this.metrics.cls = metric.value);
});
}
collectCustomMetrics() {
// Tempo até interatividade
window.addEventListener('load', () => {
this.metrics.ttl = performance.now();
});
}
sendMetrics() {
// Enviar quando página for fechada
window.addEventListener('beforeunload', () => {
navigator.sendBeacon('/api/metrics', JSON.stringify(this.metrics));
});
}
}
new PerformanceCollector();
// Enviar métricas para GA
function sendToGA(metricName, value) {
gtag('event', metricName, {
value: Math.round(value),
event_category: 'Performance',
non_interaction: true
});
}
// Enviar para seu backend
async function sendMetrics(metrics) {
try {
await fetch('/api/performance', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
...metrics,
timestamp: Date.now(),
url: window.location.href,
userAgent: navigator.userAgent
})
});
} catch (error) {
console.error('Erro ao enviar métricas:', error);
}
}
// Alertar se métricas estiverem ruins
function checkMetrics() {
const thresholds = {
lcp: 2500, // 2.5s
fid: 100, // 100ms
cls: 0.1 // 0.1
};
if (this.metrics.lcp > thresholds.lcp) {
console.warn('LCP acima do threshold:', this.metrics.lcp);
// Enviar alerta
}
if (this.metrics.fid > thresholds.fid) {
console.warn('FID acima do threshold:', this.metrics.fid);
}
if (this.metrics.cls > thresholds.cls) {
console.warn('CLS acima do threshold:', this.metrics.cls);
}
}
const budget = {
lcp: 2500,
fid: 100,
cls: 0.1,
jsSize: 200 * 1024, // 200KB
cssSize: 50 * 1024, // 50KB
imageSize: 500 * 1024 // 500KB
};
function checkBudget() {
// Verificar tamanhos
const resources = performance.getEntriesByType('resource');
const jsSize = resources
.filter(r => r.name.endsWith('.js'))
.reduce((sum, r) => sum + r.transferSize, 0);
if (jsSize > budget.jsSize) {
console.warn('JS excede orçamento:', jsSize);
}
}
# .github/workflows/lighthouse.yml
name: Lighthouse CI
on: [push, pull_request]
jobs:
lighthouse:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
- run: npm install
- run: npm run build
- uses: treosh/lighthouse-ci-action@v7
with:
urls: |
http://localhost:3000
uploadArtifacts: true
temporaryPublicStorage: true
// Dashboard simples
function createDashboard(metrics) {
const dashboard = document.createElement('div');
dashboard.innerHTML = `
<h2>Performance Metrics</h2>
<div>
<p>LCP: ${metrics.lcp}ms</p>
<p>FID: ${metrics.fid}ms</p>
<p>CLS: ${metrics.cls}</p>
</div>
`;
document.body.appendChild(dashboard);
}
// Sistema completo de monitoramento
import { onLCP, onFID, onCLS } from 'web-vitals';
class PerformanceMonitor {
constructor() {
this.metrics = {};
this.init();
}
init() {
// Coletar Web Vitals
onLCP((metric) => this.recordMetric('lcp', metric));
onFID((metric) => this.recordMetric('fid', metric));
onCLS((metric) => this.recordMetric('cls', metric));
// Coletar métricas customizadas
this.collectCustomMetrics();
// Enviar métricas
this.setupReporting();
}
recordMetric(name, metric) {
this.metrics[name] = {
value: metric.value,
rating: metric.rating,
delta: metric.delta
};
// Verificar thresholds
this.checkThresholds(name, metric.value);
}
checkThresholds(name, value) {
const thresholds = {
lcp: { good: 2500, needsImprovement: 4000 },
fid: { good: 100, needsImprovement: 300 },
cls: { good: 0.1, needsImprovement: 0.25 }
};
const threshold = thresholds[name];
if (value > threshold.needsImprovement) {
console.error(`${name} está ruim: ${value}`);
this.sendAlert(name, value);
}
}
collectCustomMetrics() {
// TTFB
const ttfb = performance.timing.responseStart -
performance.timing.requestStart;
this.metrics.ttfb = ttfb;
// Tamanho total
const resources = performance.getEntriesByType('resource');
const totalSize = resources.reduce((sum, r) =>
sum + r.transferSize, 0);
this.metrics.totalSize = totalSize;
}
setupReporting() {
// Enviar quando página fechar
window.addEventListener('beforeunload', () => {
this.sendMetrics();
});
// Ou enviar periodicamente
setInterval(() => {
this.sendMetrics();
}, 60000); // A cada minuto
}
sendMetrics() {
const data = {
...this.metrics,
url: window.location.href,
timestamp: Date.now(),
userAgent: navigator.userAgent
};
// Usar sendBeacon para garantir envio
navigator.sendBeacon('/api/metrics', JSON.stringify(data));
}
sendAlert(name, value) {
// Enviar alerta crítico
fetch('/api/alerts', {
method: 'POST',
body: JSON.stringify({ name, value, url: window.location.href })
});
}
}
// Inicializar
new PerformanceMonitor();