Web Vitals: medindo performance real do usuário
1. O que são Web Vitals e por que monitorá-las
Web Vitals são um conjunto de métricas definidas pelo Google para quantificar a experiência real do usuário em aplicações web. As Core Web Vitals incluem três métricas principais:
- LCP (Largest Contentful Paint): mede o tempo até o maior elemento visível ser renderizado — ideal abaixo de 2,5 segundos
- FID (First Input Delay) / INP (Interaction to Next Paint): mede a responsividade a interações do usuário — ideal abaixo de 100ms
- CLS (Cumulative Layout Shift): mede estabilidade visual — ideal abaixo de 0,1
Métricas complementares como FCP (First Contentful Paint) e TTFB (Time to First Byte) ajudam a diagnosticar gargalos específicos.
O Google usa essas métricas como fator de ranqueamento no Page Experience Update. Diferente de métricas de laboratório (Lighthouse), Web Vitals são métricas de campo (RUM — Real User Monitoring), coletadas de usuários reais em produção.
2. Coletando Web Vitals no navegador com JavaScript puro
A Performance API expõe métricas via PerformanceObserver. Exemplo prático de coleta:
// web-vitals-collector.js
function collectWebVitals(callback) {
// LCP
const lcpObserver = new PerformanceObserver((list) => {
const entries = list.getEntries();
const lastEntry = entries[entries.length - 1];
callback('LCP', lastEntry.startTime);
});
lcpObserver.observe({ type: 'largest-contentful-paint', buffered: true });
// FID
const fidObserver = new PerformanceObserver((list) => {
list.getEntries().forEach((entry) => {
callback('FID', entry.processingStart - entry.startTime);
});
});
fidObserver.observe({ type: 'first-input', buffered: true });
// CLS
let clsValue = 0;
const clsObserver = new PerformanceObserver((list) => {
list.getEntries().forEach((entry) => {
if (!entry.hadRecentInput) {
clsValue += entry.value;
}
});
callback('CLS', clsValue);
});
clsObserver.observe({ type: 'layout-shift', buffered: true });
}
Para navegadores legados, utilize polyfills como o pacote web-vitals que abstrai essas complexidades.
3. Integração com React: hooks e componentes para monitoramento
Crie um hook customizado para integrar Web Vitals ao ciclo de vida do React:
// useWebVitals.js
import { useEffect, useRef } from 'react';
export function useWebVitals(onReport) {
const callbackRef = useRef(onReport);
callbackRef.current = onReport;
useEffect(() => {
const handleVital = (metric) => {
callbackRef.current(metric);
};
// Import dinâmico do pacote web-vitals
import('web-vitals').then(({ onLCP, onFID, onCLS }) => {
onLCP(handleVital);
onFID(handleVital);
onCLS(handleVital);
});
}, []);
}
Componente WebVitalsReporter para enviar dados ao backend:
// WebVitalsReporter.js
import { useWebVitals } from './useWebVitals';
function WebVitalsReporter({ endpoint }) {
useWebVitals((metric) => {
const body = {
name: metric.name,
value: metric.value,
rating: metric.rating,
url: window.location.pathname,
timestamp: Date.now(),
};
// Envio via Beacon API para não bloquear o descarregamento da página
if (navigator.sendBeacon) {
navigator.sendBeacon(endpoint, JSON.stringify(body));
} else {
fetch(endpoint, {
method: 'POST',
body: JSON.stringify(body),
headers: { 'Content-Type': 'application/json' },
keepalive: true,
});
}
});
return null; // Componente não renderiza nada visualmente
}
4. Enviando métricas para o backend com Node.js
Endpoint REST no Express para receber e validar métricas:
// server.js
const express = require('express');
const { body, validationResult } = require('express-validator');
const Queue = require('bull');
const app = express();
app.use(express.json());
const vitalsQueue = new Queue('web-vitals', 'redis://localhost:6379');
app.post('/api/vitals',
[
body('name').isIn(['LCP', 'FID', 'CLS', 'FCP', 'TTFB']),
body('value').isNumeric(),
body('rating').isIn(['good', 'needs-improvement', 'poor']),
],
async (req, res) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}
// Adiciona à fila para processamento assíncrono
await vitalsQueue.add(req.body);
res.status(202).json({ status: 'accepted' });
}
);
5. Análise e visualização dos dados coletados
Processamento das métricas no backend para cálculo de percentis:
// vitals-processor.js
const { Worker } = require('bull');
const vitalsWorker = new Worker('web-vitals', async (job) => {
const { name, value, url, timestamp } = job.data;
// Armazena no banco (exemplo com MongoDB)
await Metric.create({ name, value, url, timestamp });
});
// Rota para dados agregados
app.get('/api/vitals/summary', async (req, res) => {
const metrics = await Metric.aggregate([
{ $match: { name: 'LCP' } },
{ $group: {
_id: null,
p75: { $percentile: [0.75, '$value'] },
p95: { $percentile: [0.95, '$value'] },
count: { $sum: 1 }
}}
]);
res.json(metrics);
});
6. Otimização orientada por Web Vitals no ecossistema React + Node.js
Para melhorar LCP:
- Lazy loading de imagens com loading="lazy"
- Pré-carregamento de fontes críticas via <link rel="preload">
- SSR com Next.js ou Gatsby para renderizar conteúdo no servidor
Para reduzir CLS:
- Reserve espaço para anúncios e imagens com atributos width e height
- Use font-display: swap para web fonts
- Evite inserir conteúdo dinâmico acima do conteúdo já renderizado
Para melhorar FID/INP:
- Code splitting com React.lazy() e Suspense
- Debouncing em eventos de input e scroll
- Otimização de bundles com tree shaking e code splitting
7. Monitoramento contínuo e alertas
Configuração de alertas no backend com agendamento:
// alerts.js
const cron = require('node-cron');
const { sendSlackAlert } = require('./notifications');
cron.schedule('*/5 * * * *', async () => {
const metrics = await Metric.aggregate([
{ $match: { name: 'LCP', timestamp: { $gte: Date.now() - 3600000 } } },
{ $group: { _id: null, p95: { $percentile: [0.95, '$value'] } } }
]);
if (metrics[0] && metrics[0].p95 > 4000) {
await sendSlackAlert(`⚠️ LCP p95 ultrapassou 4s: ${metrics[0].p95}ms`);
}
});
8. Caso prático: integração completa em uma aplicação React + Node.js
WebVitalsProvider que envolve toda a aplicação:
// WebVitalsProvider.js
import { useWebVitals } from './useWebVitals';
export function WebVitalsProvider({ children, endpoint }) {
useWebVitals((metric) => {
// Envia para backend e também para Google Analytics 4
if (window.gtag) {
gtag('event', 'web_vitals', {
event_category: metric.name,
value: Math.round(metric.value),
label: metric.rating,
});
}
fetch(endpoint, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(metric),
keepalive: true,
});
});
return children;
}
Uso no App principal:
// App.js
function App() {
return (
<WebVitalsProvider endpoint="/api/vitals">
<Router>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/product/:id" element={<Product />} />
</Routes>
</Router>
</WebVitalsProvider>
);
}
Para testes de carga simulando usuários reais:
npx autocannon -c 100 -d 30 http://localhost:3000
Isso gerará tráfego que permite validar se as métricas coletadas refletem a experiência real sob carga.
Referências
- Web Vitals | web.dev — Guia oficial do Google sobre métricas de performance centradas no usuário
- PerformanceObserver API | MDN — Documentação completa da API para observar métricas de performance no navegador
- web-vitals library | npm — Pacote oficial do Google para coletar Core Web Vitals com polyfills inclusos
- Using the Performance API | CSS-Tricks — Tutorial prático sobre medição de performance com JavaScript puro
- Optimize Web Vitals in React | LogRocket — Guia específico para otimização de Web Vitals em aplicações React
- Bull Queue | npm — Documentação da biblioteca de filas baseada em Redis para processamento assíncrono
- Autocannon | GitHub — Ferramenta de teste de carga HTTP para simular múltiplos usuários simultâneos