Performance e Otimização
Aula 7 de 8
React.memo
Evita re-renderizações desnecessárias em componentes puros:
import { memo } from "react";
interface ItemProps {
nome: string;
preco: number;
}
const Item = memo(function Item({ nome, preco }: ItemProps) {
console.log(`Renderizando ${nome}`);
return (
<div>
<span>{nome}</span>
<span>R$ {preco.toFixed(2)}</span>
</div>
);
});
function Lista() {
const [filtro, setFiltro] = useState("");
const items = useMemo(() => [
{ id: 1, nome: "Produto A", preco: 10 },
{ id: 2, nome: "Produto B", preco: 20 },
], []);
return (
<div>
<input value={filtro} onChange={(e) => setFiltro(e.target.value)} />
{items.map((item) => (
<Item key={item.id} nome={item.nome} preco={item.preco} />
))}
</div>
);
}
areEqual
const Item = memo(
function Item({ nome, preco }: ItemProps) {
return <div>{nome} - R$ {preco}</div>;
},
(prevProps, nextProps) => {
return prevProps.nome === nextProps.nome
&& prevProps.preco === nextProps.preco;
}
);
useCallback/useMemo Profiling
import { useMemo, useCallback } from "react";
function Dashboard({ dados }: { dados: number[] }) {
const media = useMemo(() => {
console.log("Calculando média...");
return dados.reduce((a, b) => a + b, 0) / dados.length;
}, [dados]);
const atualizar = useCallback(() => {
console.log("Atualizando...");
}, []);
return (
<div>
<p>Média: {media}</p>
<button onClick={atualizar}>Atualizar</button>
</div>
);
}
React DevTools Profiler
# Chrome: React Developer Tools
# Profiler > Record > Interagir > Analisar flamegraph
Suspense e lazy
import { lazy, Suspense } from "react";
const ComponentePesado = lazy(() => import("./ComponentePesado"));
function App() {
return (
<Suspense fallback={<div>Carregando...</div>}>
<ComponentePesado />
</Suspense>
);
}
Code Splitting com React Router
import { lazy, Suspense } from "react";
import { BrowserRouter, Routes, Route } from "react-router-dom";
const Home = lazy(() => import("./pages/Home"));
const Sobre = lazy(() => import("./pages/Sobre"));
const Dashboard = lazy(() => import("./pages/Dashboard"));
function App() {
return (
<BrowserRouter>
<Suspense fallback={<div>Carregando...</div>}>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/sobre" element={<Sobre />} />
<Route path="/dashboard" element={<Dashboard />} />
</Routes>
</Suspense>
</BrowserRouter>
);
}
Virtualização (react-window)
npm install react-window @types/react-window
import { FixedSizeList } from "react-window";
function ListaVirtualizada({ items }: { items: string[] }) {
const Row = ({ index, style }: { index: number; style: React.CSSProperties }) => (
<div style={style}>
Item #{index}: {items[index]}
</div>
);
return (
<FixedSizeList
height={400}
itemCount={items.length}
itemSize={35}
width={300}
>
{Row}
</FixedSizeList>
);
}
function App() {
const items = Array.from({ length: 100000 }, (_, i) => `Item ${i}`);
return <ListaVirtualizada items={items} />;
}
Debounce e Throttle
import { useState, useEffect } from "react";
function useDebounce<T>(valor: T, delay: number): T {
const [debounced, setDebounced] = useState(valor);
useEffect(() => {
const timer = setTimeout(() => setDebounced(valor), delay);
return () => clearTimeout(timer);
}, [valor, delay]);
return debounced;
}
function BuscaAutocomplete() {
const [query, setQuery] = useState("");
const debouncedQuery = useDebounce(query, 300);
const { dados } = useFetch(`/api/search?q=${debouncedQuery}`);
return (
<div>
<input
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Buscar..."
/>
<ul>
{(dados as string[])?.map((item) => (
<li key={item}>{item}</li>
))}
</ul>
</div>
);
}
Bundle Analysis
npm install --save-dev rollup-plugin-visualizer
import { visualizer } from "rollup-plugin-visualizer";
export default defineConfig({
plugins: [
react(),
visualizer({
filename: "./dist/stats.html",
open: true,
}),
],
});
npm run build
Estratégias de Redução de Bundle
// Tree-shakeable: importação seletiva de date-fns
import parse from "date-fns/parse";
// Code splitting por rota
const Pagina = lazy(() => import("./pages/Pagina"));
// Dynamic import condicional
const modulo = await import("./modulo-pesado");
Lab: Otimizar Lista de Produtos
import { useState, useMemo, memo, useCallback } from "react";
import { FixedSizeList } from "react-window";
import { useDebounce } from "./hooks/useDebounce";
const ProdutoCard = memo(function ProdutoCard({
nome,
preco,
onComprar,
}: {
nome: string;
preco: number;
onComprar: (nome: string) => void;
}) {
return (
<div>
<span>{nome}</span>
<span>R$ {preco.toFixed(2)}</span>
<button onClick={() => onComprar(nome)}>Comprar</button>
</div>
);
});
function App() {
const [busca, setBusca] = useState("");
const debouncedBusca = useDebounce(busca, 300);
const produtos = useMemo(() =>
Array.from({ length: 10000 }, (_, i) => ({
id: i,
nome: "Produto " + i,
preco: Math.random() * 1000,
})),
[]);
const filtrados = useMemo(
() => produtos.filter((p) =>
p.nome.toLowerCase().includes(debouncedBusca.toLowerCase())
),
[produtos, debouncedBusca]
);
const handleComprar = useCallback((nome: string) => {
console.log("Comprou: " + nome);
}, []);
const Row = useCallback(
({ index, style }: { index: number; style: React.CSSProperties }) => {
const p = filtrados[index];
return (
<div style={style}>
<ProdutoCard
nome={p.nome}
preco={p.preco}
onComprar={handleComprar}
/>
</div>
);
},
[filtrados, handleComprar]
);
return (
<div>
<input value={busca} onChange={(e) => setBusca(e.target.value)} />
<FixedSizeList
height={600}
itemCount={filtrados.length}
itemSize={50}
width={400}
>
{Row}
</FixedSizeList>
</div>
);
}
npm install react-window @types/react-window
npm run build
npm run preview
Otimize apenas o que o profiler mostra como problema. Memoização tem custo: useCallback/useMemo/Memo sem necessidade adicionam complexidade sem benefício. Meça antes, otimize depois.