Truques para melhorar performance em React

A otimização de performance em React é um tópico essencial para desenvolvedores que buscam criar aplicações rápidas e responsivas. Neste artigo, exploraremos truques práticos para melhorar a performance, cobrindo desde memorização até profiling avançado.

1. Minimizando Re-renderizações com Memorização

Re-renderizações desnecessárias são uma das principais causas de lentidão em aplicações React. Felizmente, o React oferece ferramentas nativas para evitar esse problema.

React.memo

React.memo é um higher-order component que impede re-renderizações de componentes puros quando suas props não mudam:

import React from 'react';

const ExpensiveComponent = React.memo(({ data }) => {
  console.log('Renderizou!');
  return <div>{data}</div>;
});

function App() {
  const [count, setCount] = React.useState(0);
  const data = React.useMemo(() => ({ value: 'estável' }), []);

  return (
    <div>
      <button onClick={() => setCount(count + 1)}>Clique</button>
      <ExpensiveComponent data={data} />
    </div>
  );
}

useMemo e useCallback

useMemo memoriza valores calculados, enquanto useCallback estabiliza referências de funções:

import React, { useMemo, useCallback } from 'react';

function SearchResults({ query, items }) {
  const filteredItems = useMemo(() => {
    return items.filter(item => item.includes(query));
  }, [query, items]);

  const handleClick = useCallback((id) => {
    console.log('Item clicado:', id);
  }, []);

  return (
    <ul>
      {filteredItems.map(item => (
        <li key={item.id} onClick={() => handleClick(item.id)}>
          {item.name}
        </li>
      ))}
    </ul>
  );
}

2. Otimização de Listas e Grandes Conjuntos de Dados

Renderizar listas enormes pode degradar drasticamente a performance. A virtualização é a solução ideal.

Virtualização com react-window

import { FixedSizeList as List } from 'react-window';

const Row = ({ index, style }) => (
  <div style={style}>Item {index + 1}</div>
);

function VirtualizedList() {
  return (
    <List
      height={400}
      itemCount={10000}
      itemSize={35}
      width={300}
    >
      {Row}
    </List>
  );
}

Key prop correta

Sempre use keys estáveis e únicas para evitar reconciliação ineficiente:

// Ruim
{items.map((item, index) => <Item key={index} />)}

// Bom
{items.map(item => <Item key={item.id} />)}

Lazy loading com Intersection Observer

import { useEffect, useRef, useState } from 'react';

function LazyImage({ src, alt }) {
  const [isVisible, setIsVisible] = useState(false);
  const imgRef = useRef();

  useEffect(() => {
    const observer = new IntersectionObserver(([entry]) => {
      if (entry.isIntersecting) {
        setIsVisible(true);
        observer.disconnect();
      }
    });
    observer.observe(imgRef.current);
    return () => observer.disconnect();
  }, []);

  return (
    <div ref={imgRef}>
      {isVisible ? <img src={src} alt={alt} /> : <div>Carregando...</div>}
    </div>
  );
}

3. Gerenciamento Eficiente de Estado e Contexto

Contextos mal estruturados podem causar re-renderizações em cascata.

Divisão de Contextos

Em vez de um contexto gigante, divida em contextos menores:

const UserContext = React.createContext();
const ThemeContext = React.createContext();

function App() {
  return (
    <UserContext.Provider value={user}>
      <ThemeContext.Provider value={theme}>
        <MainComponent />
      </ThemeContext.Provider>
    </UserContext.Provider>
  );
}

Zustand para estado atômico

import { create } from 'zustand';

const useStore = create((set) => ({
  count: 0,
  increment: () => set((state) => ({ count: state.count + 1 })),
}));

function Counter() {
  const count = useStore((state) => state.count);
  return <div>{count}</div>;
}

4. Code Splitting e Lazy Loading de Componentes

Reduza o bundle inicial carregando componentes sob demanda.

React.lazy e Suspense

import React, { Suspense } from 'react';

const HeavyComponent = React.lazy(() => import('./HeavyComponent'));

function App() {
  return (
    <Suspense fallback={<div>Carregando...</div>}>
      <HeavyComponent />
    </Suspense>
  );
}

Lazy loading com React Router

import { BrowserRouter, Routes, Route } from 'react-router-dom';
import React, { Suspense } from 'react';

const Home = React.lazy(() => import('./pages/Home'));
const Dashboard = React.lazy(() => import('./pages/Dashboard'));

function App() {
  return (
    <BrowserRouter>
      <Suspense fallback={<div>Carregando...</div>}>
        <Routes>
          <Route path="/" element={<Home />} />
          <Route path="/dashboard" element={<Dashboard />} />
        </Routes>
      </Suspense>
    </BrowserRouter>
  );
}

5. Otimização de Imagens e Assets

Imagens são frequentemente os maiores assets em uma página.

Lazy loading nativo

<img src="large-image.jpg" loading="lazy" alt="Descrição" />

Pré-carregamento de imagens críticas

<link rel="preload" href="hero-image.webp" as="image" />

6. Profiling e Identificação de Gargalos

Use ferramentas para identificar problemas de performance.

React DevTools Profiler

// No console do navegador com React DevTools
// Acesse a aba "Profiler" e grave interações

why-did-you-render

import React from 'react';

if (process.env.NODE_ENV === 'development') {
  const whyDidYouRender = require('@welldone-software/why-did-you-render');
  whyDidYouRender(React, {
    trackAllPureComponents: true,
  });
}

7. Boas Práticas de Renderização e Estilização

Evitar funções anônimas em props

// Ruim
<Button onClick={() => handleClick(id)} />

// Bom
<Button onClick={handleClick} />

useRef para valores mutáveis

function Timer() {
  const countRef = useRef(0);

  useEffect(() => {
    const interval = setInterval(() => {
      countRef.current += 1;
    }, 1000);
    return () => clearInterval(interval);
  }, []);

  return <div>Timer rodando...</div>;
}

Conclusão

A otimização de performance em React envolve múltiplas estratégias, desde memorização até virtualização e code splitting. Comece identificando gargalos com o Profiler, aplique memorização onde necessário, virtualize listas grandes e divida seu código em chunks menores. Lembre-se: otimize apenas quando houver problemas reais de performance, evitando complexidade desnecessária.

Referências