React & Frontend

Como Usar Web Vitals em Aplicações React: LCP, CLS, INP e Otimizações em Produção

17 min de leitura

Como Usar Web Vitals em Aplicações React: LCP, CLS, INP e Otimizações em Produção

Web Vitals em Aplicações React: Entendendo as Métricas Essenciais Os Web Vitals são métricas de desempenho estabelecidas pelo Google para medir a experiência real do usuário em uma aplicação web. Em React, onde interatividade é fundamental, entender e otimizar essas métricas é absolutamente crítico. Diferentemente de métricas tradicionais como tempo de carregamento genérico, os Web Vitals focam especificamente no que importa: quando o usuário consegue ver o conteúdo, quando consegue interagir com ele, e se a página permanece estável visualmente enquanto isso acontece. Neste artigo, abordaremos três métricas Core Web Vitals: LCP (Largest Contentful Paint), CLS (Cumulative Layout Shift) e INP (Interaction to Next Paint). Cada uma delas representa um pilar diferente da experiência do usuário. LCP mede a velocidade percebida de carregamento, CLS mede a estabilidade visual, e INP mede a responsividade da aplicação. Entender como cada uma funciona e como otimizá-las em React é essencial para construir aplicações que não apenas funcionam bem, mas que são percebidas como

<h2>Web Vitals em Aplicações React: Entendendo as Métricas Essenciais</h2>

<p>Os Web Vitals são métricas de desempenho estabelecidas pelo Google para medir a experiência real do usuário em uma aplicação web. Em React, onde interatividade é fundamental, entender e otimizar essas métricas é absolutamente crítico. Diferentemente de métricas tradicionais como tempo de carregamento genérico, os Web Vitals focam especificamente no que importa: quando o usuário consegue ver o conteúdo, quando consegue interagir com ele, e se a página permanece estável visualmente enquanto isso acontece.</p>

<p>Neste artigo, abordaremos três métricas Core Web Vitals: LCP (Largest Contentful Paint), CLS (Cumulative Layout Shift) e INP (Interaction to Next Paint). Cada uma delas representa um pilar diferente da experiência do usuário. LCP mede a velocidade percebida de carregamento, CLS mede a estabilidade visual, e INP mede a responsividade da aplicação. Entender como cada uma funciona e como otimizá-las em React é essencial para construir aplicações que não apenas funcionam bem, mas que são <strong>percebidas</strong> como rápidas e fluidas pelos usuários finais.</p>

<h2>LCP (Largest Contentful Paint)</h2>

<h3>O que é LCP</h3>

<p>LCP mede o tempo até que o maior elemento visível (geralmente uma imagem ou texto em bloco) seja renderizado na viewport. Tecnicamente, é quando esse elemento maior se torna &quot;pintado&quot; (painted) no navegador. Um bom LCP é abaixo de 2.5 segundos. Valores entre 2.5 e 4 segundos precisam de melhoria, e acima de 4 segundos é considerado ruim.</p>

<p>A questão fundamental aqui é: o usuário consegue ver algo significativo na tela rapidamente? Em aplicações React, onde o JavaScript controla a renderização, é comum que o LCP seja afetado pelo tempo necessário para fazer download, parse e execução do bundle JavaScript antes que o conteúdo principal apareça.</p>

<h3>Identificando Problemas de LCP</h3>

<p>Para diagnosticar problemas de LCP em sua aplicação React, use o Chrome DevTools ou a API <code>PerformanceObserver</code>. O código abaixo captura dados reais de LCP:</p>

<pre><code class="language-javascript">// Hook para medir LCP em React

import { useEffect } from &#039;react&#039;;

const useLCPMetric = () =&gt; {

useEffect(() =&gt; {

const observer = new PerformanceObserver((entryList) =&gt; {

const entries = entryList.getEntries();

const lastEntry = entries[entries.length - 1];

console.log(&#039;LCP:&#039;, lastEntry.renderTime || lastEntry.loadTime);

console.log(&#039;LCP Element:&#039;, lastEntry.element);

});

observer.observe({ type: &#039;largest-contentful-paint&#039;, buffered: true });

return () =&gt; observer.disconnect();

}, []);

};

export default useLCPMetric;</code></pre>

<p>Este hook, quando utilizado no seu componente raiz, exibirá no console qual elemento é considerado o &quot;maior conteúdo&quot; e em quanto tempo ele foi renderizado. Frequentemente você descobrirá que a culpada é uma imagem grande ou um componente que depende de dados vindos de uma API.</p>

<h3>Otimizações Práticas para LCP</h3>

<p><strong>1. Code Splitting e Lazy Loading:</strong> Ao invés de carregar todo o JavaScript de uma vez, divida seu bundle React em partes menores que são carregadas conforme necessário.</p>

<pre><code class="language-javascript">// src/App.jsx

import { lazy, Suspense } from &#039;react&#039;;

const HeavyDashboard = lazy(() =&gt; import(&#039;./pages/HeavyDashboard&#039;));

const ProductList = lazy(() =&gt; import(&#039;./pages/ProductList&#039;));

function App() {

return (

&lt;Suspense fallback={&lt;div&gt;Carregando...&lt;/div&gt;}&gt;

&lt;Routes&gt;

&lt;Route path=&quot;/dashboard&quot; element={&lt;HeavyDashboard /&gt;} /&gt;

&lt;Route path=&quot;/products&quot; element={&lt;ProductList /&gt;} /&gt;

&lt;/Routes&gt;

&lt;/Suspense&gt;

);

}</code></pre>

<p><strong>2. Otimizar Imagens do LCP:</strong> Se a imagem é o maior conteúdo, comprima-a, use formatos modernos (WebP) e carregue versões responsivas:</p>

<pre><code class="language-javascript">// Componente com imagem otimizada para LCP

function HeroImage() {

return (

&lt;picture&gt;

&lt;source srcSet=&quot;/hero.webp&quot; type=&quot;image/webp&quot; /&gt;

&lt;source srcSet=&quot;/hero.jpg&quot; type=&quot;image/jpeg&quot; /&gt;

&lt;img

src=&quot;/hero.jpg&quot;

alt=&quot;Hero&quot;

loading=&quot;eager&quot;

fetchPriority=&quot;high&quot;

width=&quot;1200&quot;

height=&quot;600&quot;

style={{ width: &#039;100%&#039;, height: &#039;auto&#039; }}

/&gt;

&lt;/picture&gt;

);

}</code></pre>

<p>O atributo <code>fetchPriority=&quot;high&quot;</code> instruí o navegador a priorizar essa imagem no download. <code>loading=&quot;eager&quot;</code> garante que ela começa a carregar imediatamente.</p>

<p><strong>3. Server-Side Rendering (SSR):</strong> Render a página no servidor para que o conteúdo principal chegue ao navegador já pronto, sem depender de JavaScript executar primeiro.</p>

<pre><code class="language-javascript">// server.js (com Express e React)

import express from &#039;express&#039;;

import { renderToString } from &#039;react-dom/server&#039;;

import App from &#039;./App.jsx&#039;;

const app = express();

app.get(&#039;/&#039;, (req, res) =&gt; {

const html = renderToString(&lt;App /&gt;);

res.send(`

&lt;!DOCTYPE html&gt;

&lt;html&gt;

&lt;head&gt;

&lt;title&gt;Minha App&lt;/title&gt;

&lt;/head&gt;

&lt;body&gt;

&lt;div id=&quot;root&quot;&gt;${html}&lt;/div&gt;

&lt;script src=&quot;/bundle.js&quot;&gt;&lt;/script&gt;

&lt;/body&gt;

&lt;/html&gt;

`);

});

app.listen(3000);</code></pre>

<p>Com SSR, o navegador renderiza HTML puro imediatamente, enquanto o JavaScript para interatividade carrega em paralelo.</p>

<h2>CLS (Cumulative Layout Shift)</h2>

<h3>O que é CLS</h3>

<p>CLS mede quanto a página &quot;pula&quot; ou se move inesperadamente enquanto o usuário está interagindo com ela. Imagine você lendo um artigo e de repente um anúncio carrega empurrando o texto para baixo — isso é layout shift negativo. Um bom CLS é abaixo de 0.1, entre 0.1 e 0.25 precisa melhorar, e acima disso é ruim.</p>

<p>A fórmula técnica envolve o quanto de espaço foi deslocado multiplicado pela fração da viewport que foi afetada, mas o importante para você saber é: <strong>reserve espaço antecipadamente para elementos que serão carregados depois</strong>. Em React, onde componentes podem aparecer dinamicamente, isso é uma preocupação constante.</p>

<h3>Diagnosticando CLS</h3>

<pre><code class="language-javascript">// Hook para detectar layout shifts

import { useEffect } from &#039;react&#039;;

const useCLSMetric = () =&gt; {

useEffect(() =&gt; {

let clsValue = 0;

const observer = new PerformanceObserver((list) =&gt; {

for (const entry of list.getEntries()) {

if (!entry.hadRecentInput) {

clsValue += entry.value;

console.log(&#039;Current CLS:&#039;, clsValue);

}

}

});

observer.observe({ type: &#039;layout-shift&#039;, buffered: true });

return () =&gt; observer.disconnect();

}, []);

};

export default useCLSMetric;</code></pre>

<p>Execure este hook e interaja com sua página. Abra o console e observe quanto ela &quot;pula&quot;. A propriedade <code>hadRecentInput</code> garante que shifts causados por ações do usuário (como digitar em um input) sejam ignorados, já que esses são esperados.</p>

<h3>Práticas para Eliminar CLS</h3>

<p><strong>1. Reserve Espaço para Imagens e Iframes:</strong></p>

<pre><code class="language-javascript"></code></pre>

<p><strong>2. Evite Inserir Conteúdo Acima do Já Carregado:</strong></p>

<pre><code class="language-javascript"></code></pre>

<p><strong>3. Use <code>transform</code> Ao Invés de Propriedades que Causam Reflow:</strong></p>

<pre><code class="language-javascript"></code></pre>

<p><strong>4. Estabilize Componentes Dinâmicos:</strong></p>

<pre><code class="language-javascript">function UserMenu() {

const [isOpen, setIsOpen] = useState(false);

return (

&lt;div style={{ position: &#039;relative&#039; }}&gt;

&lt;button onClick={() =&gt; setIsOpen(!isOpen)}&gt;Menu&lt;/button&gt;

{/ Reserve espaço fixo para o dropdown, ele não empurra nada /}

&lt;div

style={{

position: &#039;absolute&#039;,

top: &#039;100%&#039;,

minWidth: &#039;200px&#039;,

minHeight: isOpen ? &#039;150px&#039; : &#039;0px&#039;,

}}

&gt;

{isOpen &amp;&amp; (

&lt;ul&gt;

&lt;li&gt;&lt;a href=&quot;/profile&quot;&gt;Perfil&lt;/a&gt;&lt;/li&gt;

&lt;li&gt;&lt;a href=&quot;/logout&quot;&gt;Sair&lt;/a&gt;&lt;/li&gt;

&lt;/ul&gt;

)}

&lt;/div&gt;

&lt;/div&gt;

);

}</code></pre>

<h2>INP (Interaction to Next Paint)</h2>

<h3>O que é INP</h3>

<p>INP mede o tempo entre o usuário fazer algo (clicar, digitar, tocar) e o navegador exibir a próxima atualização visual em resposta. Um bom INP é abaixo de 200ms, entre 200 e 500ms precisa melhorar, e acima é ruim. Diferentemente de LCP que mede load e CLS que mede visual stability, INP mede <strong>responsividade</strong>.</p>

<p>Em aplicações React, INP é frequentemente degradado por lógica pesada em event handlers, renderizações custosas de componentes, ou ambos acontecendo sincronamente. A solução envolve quebrar trabalho pesado em pedaços menores usando técnicas como <code>startTransition</code>, Web Workers, ou simples debouncing.</p>

<h3>Medindo INP</h3>

<pre><code class="language-javascript">// Hook para monitorar INP

import { useEffect } from &#039;react&#039;;

const useINPMetric = () =&gt; {

useEffect(() =&gt; {

const observer = new PerformanceObserver((list) =&gt; {

for (const entry of list.getEntries()) {

console.log(&#039;INP:&#039;, entry.duration);

console.log(&#039;Interaction Type:&#039;, entry.name);

}

});

observer.observe({ type: &#039;event&#039;, buffered: true });

return () =&gt; observer.disconnect();

}, []);

};

export default useINPMetric;</code></pre>

<h3>Otimizando INP em React</h3>

<p><strong>1. Use <code>startTransition</code> para Updates Não-Urgentes:</strong></p>

<pre><code class="language-javascript">// Versão clássica - tudo é síncrono, UI trava

function SearchBad() {

const [query, setQuery] = useState(&#039;&#039;);

const [results, setResults] = useState([]);

const handleChange = (e) =&gt; {

const value = e.target.value;

setQuery(value);

// Filtrar 10k items sincronamente, UI trava

setResults(expensiveFilter(value));

};

return (

&lt;&gt;

&lt;input onChange={handleChange} /&gt;

&lt;ResultsList items={results} /&gt;

&lt;/&gt;

);

}

// Versão otimizada com startTransition

import { useState, startTransition } from &#039;react&#039;;

function SearchGood() {

const [query, setQuery] = useState(&#039;&#039;);

const [results, setResults] = useState([]);

const [isPending, setIsPending] = useState(false);

const handleChange = (e) =&gt; {

const value = e.target.value;

setQuery(value); // Atualização urgente - input responde imediatamente

// Atualização não-urgente - resultados processados no fundo

startTransition(() =&gt; {

setResults(expensiveFilter(value));

setIsPending(false);

});

setIsPending(true);

};

return (

&lt;&gt;

&lt;input onChange={handleChange} value={query} /&gt;

{isPending &amp;&amp; &lt;p&gt;Buscando...&lt;/p&gt;}

&lt;ResultsList items={results} /&gt;

&lt;/&gt;

);

}</code></pre>

<p>Com <code>startTransition</code>, o input responde imediatamente porque React prioriza a atualização de estado do <code>query</code>. A filtragem pesada acontece &quot;em background&quot; sem bloquear a interação do usuário.</p>

<p><strong>2. Debounce Operações Pesadas:</strong></p>

<pre><code class="language-javascript">// Hook customizado para debounce

function useDebounce(value, delay) {

const [debouncedValue, setDebouncedValue] = useState(value);

useEffect(() =&gt; {

const handler = setTimeout(() =&gt; {

setDebouncedValue(value);

}, delay);

return () =&gt; clearTimeout(handler);

}, [value, delay]);

return debouncedValue;

}

function DataTable({ initialData }) {

const [searchTerm, setSearchTerm] = useState(&#039;&#039;);

const debouncedSearchTerm = useDebounce(searchTerm, 300);

// Apenas calcula quando debouncedSearchTerm muda (a cada 300ms no máximo)

const filteredData = useMemo(() =&gt; {

return initialData.filter(item =&gt;

item.name.toLowerCase().includes(debouncedSearchTerm.toLowerCase())

);

}, [debouncedSearchTerm, initialData]);

return (

&lt;&gt;

&lt;input

value={searchTerm}

onChange={(e) =&gt; setSearchTerm(e.target.value)}

placeholder=&quot;Buscar...&quot;

/&gt;

&lt;Table data={filteredData} /&gt;

&lt;/&gt;

);

}</code></pre>

<p>Aqui, o usuário digita suavemente (cada keystroke é respondido imediatamente), mas a filtragem pesada só ocorre 300ms após o último keystroke.</p>

<p><strong>3. Use Web Workers para Processamento Pesado:</strong></p>

<pre><code class="language-javascript">// worker.js - executado em thread separada

self.onmessage = (event) =&gt; {

const { data } = event;

const results = processLargeDataset(data);

self.postMessage(results);

};

// Component.jsx

function DataProcessor() {

const [results, setResults] = useState([]);

const workerRef = useRef(null);

useEffect(() =&gt; {

// Cria worker apenas uma vez

workerRef.current = new Worker(new URL(&#039;./worker.js&#039;, import.meta.url));

workerRef.current.onmessage = (event) =&gt; {

setResults(event.data);

};

return () =&gt; workerRef.current.terminate();

}, []);

const handleProcess = (largeDataset) =&gt; {

// Envia dados para o worker - UI não trava

workerRef.current.postMessage(largeDataset);

};

return (

&lt;&gt;

&lt;button onClick={() =&gt; handleProcess(hugeDataArray)}&gt;Processar&lt;/button&gt;

{results &amp;&amp; &lt;DisplayResults data={results} /&gt;}

&lt;/&gt;

);

}</code></pre>

<p>Web Workers executam código em uma thread separada, completamente fora da thread principal que renderiza a UI.</p>

<p><strong>4. Implemente Virtualization para Listas Grandes:</strong></p>

<pre><code class="language-javascript">// Usando react-window para renderizar apenas itens visíveis

import { FixedSizeList as List } from &#039;react-window&#039;;

function LargeList({ items }) {

// Em vez de renderizar 10k elementos, renderiza apenas ~30 visíveis

return (

&lt;List

height={600}

itemCount={items.length}

itemSize={35}

width=&quot;100%&quot;

&gt;

{({ index, style }) =&gt; (

&lt;div style={style}&gt;

{items[index].name} - {items[index].value}

&lt;/div&gt;

)}

&lt;/List&gt;

);

}</code></pre>

<h2>Integração Completa e Monitoring</h2>

<h3>Enviando Web Vitals para um Serviço de Analytics</h3>

<p>Em produção, você quer saber como os Web Vitals estão sendo percebidos pelos usuários reais. Aqui está como enviar essas métricas para um serviço externo:</p>

<pre><code class="language-javascript">// lib/web-vitals.js

export function reportWebVitals(metric) {

// Envia para seu endpoint de analytics

fetch(&#039;/api/metrics&#039;, {

method: &#039;POST&#039;,

headers: { &#039;Content-Type&#039;: &#039;application/json&#039; },

body: JSON.stringify({

name: metric.name,

value: metric.value,

id: metric.id,

rating: metric.rating, // &#039;good&#039;, &#039;needs-improvement&#039;, &#039;poor&#039;

delta: metric.delta,

timestamp: new Date().toISOString(),

url: window.location.href,

userAgent: navigator.userAgent,

}),

}).catch(err =&gt; console.error(&#039;Analytics failed:&#039;, err));

}

// src/main.jsx

import { getCLS, getFCP, getFID, getLCP, getINP, getTTFB } from &#039;web-vitals&#039;;

import { reportWebVitals } from &#039;./lib/web-vitals&#039;;

getCLS(reportWebVitals);

getFCP(reportWebVitals);

getFID(reportWebVitals); // Descontinuado, substituído por INP

getLCP(reportWebVitals);

getINP(reportWebVitals);

getTTFB(reportWebVitals);</code></pre>

<p>Ou use uma biblioteca pronta como <code>web-vitals</code>:</p>

<pre><code class="language-bash">npm install web-vitals</code></pre>

<h3>Checklist de Otimizações</h3>

<p>Antes de considerar sua aplicação otimizada, verifique:</p>

<ul>

<li><strong>LCP:</strong> Imagens otimizadas com dimensões, code splitting implementado, considere SSR</li>

<li><strong>CLS:</strong> Todos os elementos dinâmicos têm dimensões reservadas, transforms usados ao invés de propriedades de layout</li>

<li><strong>INP:</strong> <code>startTransition</code> para updates não-urgentes, debounce implementado, trabalho pesado delegado a Web Workers</li>

</ul>

<h2>Conclusão</h2>

<p>Os Web Vitals não são apenas números — eles representam a experiência concreta do seu usuário. <strong>LCP determina se sua aplicação parece rápida</strong>, <strong>CLS determina se ela parece estável</strong>, e <strong>INP determina se ela responde aos comandos do usuário</strong>. Em React, onde o controle é fino e granular, aplicar as técnicas corretas faz toda a diferença entre uma aplicação que funciona e uma que é prazer usar.</p>

<p>A prioridade deve ser: medir primeiro com <code>PerformanceObserver</code> ou as APIs do Chrome DevTools, identificar qual métrica está ruim, depois aplicar a otimização específica para aquela métrica. Não otimize tudo de uma vez; foque no que está degradando a experiência real dos usuários.</p>

<h2>Referências</h2>

<ul>

<li><a href="https://web.dev/vitals/" target="_blank" rel="noopener noreferrer">Web Vitals - Google Developers</a></li>

<li><a href="https://developer.mozilla.org/en-US/docs/Web/Performance" target="_blank" rel="noopener noreferrer">Measuring Web Performance - MDN Web Docs</a></li>

<li><a href="https://react.dev/reference/react/useTransition" target="_blank" rel="noopener noreferrer">React 18 useTransition Hook - React Official Docs</a></li>

<li><a href="https://www.oreilly.com/library/view/building-web-performance/9781491930558/" target="_blank" rel="noopener noreferrer">Building Web Performance - O&#039;Reilly (por Estelle Weyl)</a></li>

<li><a href="https://developer.chrome.com/docs/devtools/performance/" target="_blank" rel="noopener noreferrer">Chrome DevTools Performance Tab - Google Chrome Documentation</a></li>

</ul>

<p>&lt;!-- FIM --&gt;</p>

Comentários

Mais em React & Frontend

Como Usar Observabilidade em React: Sentry, LogRocket e Error Boundaries em Produção
Como Usar Observabilidade em React: Sentry, LogRocket e Error Boundaries em Produção

O que é Observabilidade em React? Observabilidade é a capacidade de entender...

Monorepo de Componentes React: Storybook, Chromatic e Releases: Do Básico ao Avançado
Monorepo de Componentes React: Storybook, Chromatic e Releases: Do Básico ao Avançado

O Que é um Monorepo de Componentes React Um monorepo (repositório monolítico)...

Guia Completo de Concurrent Mode em React: Suspense, Transitions e useDeferredValue
Guia Completo de Concurrent Mode em React: Suspense, Transitions e useDeferredValue

Introdução ao Concurrent Mode O Concurrent Mode é uma das features mais trans...