kb.erickguedes.com
React: Construindo Interfaces Modernas

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.