O Scheduler do React: Entendendo o Motor de Renderização
O React é uma biblioteca JavaScript que gerencia a renderização eficiente de componentes em interfaces de usuário. No entanto, quando você tem centenas de componentes atualizando simultaneamente, ou operações pesadas ocorrendo em paralelo com interações do usuário, o desempenho pode degradar rapidamente. O Scheduler do React é o mecanismo responsável por organizar quando e como essas atualizações acontecem, garantindo que a aplicação permaneça responsiva.
O Scheduler não é apenas um gerenciador de fila. Ele é uma camada sofisticada que trabalha em conjunto com o algoritmo de reconciliação do React (a forma como o React descobre quais mudanças precisam ser aplicadas ao DOM). Quando você clica em um botão, digita em um input ou uma requisição HTTP completa, o Scheduler recebe essas "tarefas" e decide a ordem e o tempo ideal para executá-las, equilibrando a responsividade da interface com a completude das atualizações.
Prioridades de Renderização: O Conceito Fundamental
Por que Prioridades Existem?
Nem todas as atualizações do React são iguais. Quando você digita em um campo de input, essa atualização deve ser instantânea — é uma interação direta do usuário. Já uma atualização de dados em background (como sincronizar dados com um servidor) pode esperar alguns milissegundos sem prejudicar a experiência. O React define diferentes níveis de prioridade para distinguir essas situações.
As prioridades no React estão organizadas em um espectro, da mais alta para a mais baixa:
- ImmediatePriority (Urgente) — Sincronização com o navegador, eventos de click/touch imediatos
- UserBlockingPriority (Bloqueadora ao Usuário) — Eventos de input, hover, animações
- NormalPriority (Normal) — Atualizações de dados, requisições HTTP completadas
- LowPriority (Baixa) — Pré-carregamento, análises, logs
- IdlePriority (Ociosa) — Tarefas que podem esperar indefinidamente
Implementação Prática com Prioridades
Para trabalhar com prioridades no React moderno, você utiliza a API useTransition ou useDeferredValue, que permitem marcar atualizações como de menor prioridade. Vamos ver um exemplo real:
import { useState, useTransition } from 'react';
export default function SearchUsers() {
const [input, setInput] = useState('');
const [results, setResults] = useState([]);
// isPending indica se há uma atualização pendente em background
const [isPending, startTransition] = useTransition();
const handleSearch = (value) => {
// Atualizar o input é imediato e responsivo
setInput(value);
// A filtragem dos resultados é uma tarefa de menor prioridade
startTransition(() => {
// Simulando uma busca pesada em um grande array
const filtered = generateLargeDataset().filter(user =>
user.name.toLowerCase().includes(value.toLowerCase())
);
setResults(filtered);
});
};
return (
<div>
<input
type="text"
value={input}
onChange={(e) => handleSearch(e.target.value)}
placeholder="Buscar usuários..."
/>
{isPending && <p>Buscando...</p>}
<ul>
{results.map((user) => (
<li key={user.id}>{user.name}</li>
))}
</ul>
</div>
);
}
function generateLargeDataset() {
// Simula um grande conjunto de dados
return Array.from({ length: 100000 }, (_, i) => ({
id: i,
name: `User ${i}`,
}));
}
Neste exemplo, quando o usuário digita, setInput é chamado imediatamente (alta prioridade), mantendo o campo responsivo. A filtragem dos resultados, dentro do startTransition, é uma tarefa de menor prioridade que pode ser pausada se o usuário digitar novamente.
Time Slicing: Dividindo Trabalho em Fatias
O Problema da Renderização Bloqueante
Historicamente, quando o React precisava renderizar componentes, ele fazia tudo de uma vez. Se você tinha uma lista de 1000 itens para renderizar, o React processaria todos eles antes de devolver o controle ao navegador. Durante esse tempo, qualquer clique do usuário, digitação ou animação ficaria congelado, esperando. Essa era a essência do problema de bloqueio da thread principal (main thread).
O Time Slicing resolve isso dividindo o trabalho em pequenas "fatias" de tempo. O React renderiza um pouco, interrompe, verifica se há interações do usuário de alta prioridade, processa isso, e retorna ao trabalho de renderização. Tudo isso acontece em milissegundos, criando a ilusão de que o React está executando múltiplas tarefas simultaneamente.
Como o Time Slicing Funciona Internamente
Quando você usa useTransition ou marca algo como startTransition, o React não apenas reduz a prioridade — também fragmenta o trabalho. O Scheduler usa requestIdleCallback ou MessageChannel (dependendo do navegador) para criar essas fatias. Vamos ver um exemplo mais complexo que demonstra o efeito:
import { useState, useTransition, useCallback } from 'react';
export default function HeavyListRenderer() {
const [count, setCount] = useState(0);
const [items, setItems] = useState([]);
const [isPending, startTransition] = useTransition();
const generateItems = useCallback(() => {
startTransition(() => {
// Esta é uma operação pesada que será fatiadaem tempo
const newItems = Array.from({ length: 10000 }, (_, i) => ({
id: i,
value: Math.random() * 100,
timestamp: Date.now() + i,
}));
setItems(newItems);
});
}, []);
return (
<div style={{ padding: '20px' }}>
<h1>Contador: {count}</h1>
{/* Este botão permanece responsivo mesmo durante renderização pesada */}
<button onClick={() => setCount(c => c + 1)}>
Incrementar (sempre rápido)
</button>
<button onClick={generateItems}>
Gerar 10.000 itens
</button>
{isPending && <p>⏳ Renderizando itens...</p>}
<div style={{ marginTop: '20px' }}>
{items.map((item) => (
<div key={item.id} style={{ padding: '4px' }}>
Item {item.id}: {item.value.toFixed(2)}
</div>
))}
</div>
</div>
);
}
Sem startTransition, clicar em "Incrementar" durante a renderização dos 10.000 itens causaria um atraso visível. Com startTransition, o click é processado quase instantaneamente, pois o React pausa a renderização dos itens quando detecta uma interação de alta prioridade.
Diferença entre useDeferredValue e useTransition
Existem duas maneiras de integrar time slicing em seus componentes, e é importante conhecer a diferença:
import { useState, useTransition, useDeferredValue } from 'react';
// Exemplo 1: useTransition (para ações)
function SearchWithTransition() {
const [input, setInput] = useState('');
const [isPending, startTransition] = useTransition();
const handleChange = (e) => {
// input muda imediatamente
setInput(e.target.value);
// atualizar resultados é adiado
startTransition(() => {
// simulação de operação pesada
console.log('Processando busca para:', e.target.value);
});
};
return (
<>
<input value={input} onChange={handleChange} />
{isPending && <span>Atualizando...</span>}
</>
);
}
// Exemplo 2: useDeferredValue (para valores)
function FilterListWithDeferred() {
const [input, setInput] = useState('');
const deferredInput = useDeferredValue(input);
// O deferredInput será atualizado com atraso
// permitindo que o input em si permaneça responsivo
return (
<>
<input
value={input}
onChange={(e) => setInput(e.target.value)}
/>
<FilteredList searchTerm={deferredInput} />
</>
);
}
function FilteredList({ searchTerm }) {
// Este componente será renderizado com deferredInput
// podendo estar um passo atrás do input real
return <div>Resultados para: {searchTerm}</div>;
}
Quando usar cada um? Use useTransition quando você quer controlar explicitamente uma ação que dispara atualizações. Use useDeferredValue quando você quer que um valor seja atualizado com atraso automaticamente, sem precisar encapsular a lógica em startTransition.
Monitoramento e Medição de Performance
Identificando Gargalos
Entender as prioridades e o time slicing é uma coisa. Saber identificar quando sua aplicação não está usando esses conceitos corretamente é outra. As DevTools do React mostram quais componentes estão sendo renderizados e quanto tempo leva:
import { Profiler } from 'react';
function onRenderCallback(
id, // "SearchUsers" ou outro nome do componente
phase, // "mount" ou "update"
actualDuration, // tempo gasto renderizando este componente
baseDuration, // tempo para renderizar sem memoização
startTime, // quando React começou a renderizar
commitTime, // quando a renderização foi concluída
interactions // que transições causaram esta renderização
) {
console.log(`${id} (${phase}) levou ${actualDuration}ms`);
}
export default function App() {
return (
<Profiler id="SearchApp" onRender={onRenderCallback}>
<SearchUsers />
</Profiler>
);
}
Quando você vir que um componente está levando mais de 16ms para renderizar (o que causa perda de frames em 60fps), é um sinal de que você deve aplicar time slicing ou otimizações.
Exemplo Completo: Integração de Tudo
Vamos criar um exemplo que demonstra prioridades, time slicing e monitoramento juntos:
import {
useState,
useTransition,
useCallback,
Profiler,
} from 'react';
function CompleteExample() {
const [query, setQuery] = useState('');
const [results, setResults] = useState([]);
const [isPending, startTransition] = useTransition();
// Simula uma busca pesada em um grande dataset
const performSearch = useCallback((searchQuery) => {
startTransition(() => {
const allData = Array.from({ length: 100000 }, (_, i) => `Item ${i}`);
const filtered = allData.filter((item) =>
item.toLowerCase().includes(searchQuery.toLowerCase())
);
setResults(filtered.slice(0, 100)); // Mostra apenas os primeiros 100
});
}, []);
const handleInputChange = (e) => {
const newQuery = e.target.value;
setQuery(newQuery); // Atualiza imediatamente
performSearch(newQuery); // Busca em background
};
return (
<Profiler id="SearchApp" onRender={(id, phase, duration) => {
if (duration > 16) console.warn(`${id} levou ${duration}ms`);
}}>
<div style={{ padding: '20px', fontFamily: 'Arial' }}>
<h1>Busca com Time Slicing</h1>
<input
type="text"
value={query}
onChange={handleInputChange}
placeholder="Digite para buscar..."
style={{ padding: '8px', width: '300px' }}
/>
{isPending && <p style={{ color: 'orange' }}>🔄 Buscando...</p>}
<p>Encontrados: {results.length} resultados</p>
<ul style={{ maxHeight: '400px', overflow: 'auto' }}>
{results.map((result, idx) => (
<li key={idx}>{result}</li>
))}
</ul>
</div>
</Profiler>
);
}
export default CompleteExample;
Conclusão
Durante este artigo, você aprendeu que o React Scheduler é muito mais que um gerenciador de fila — é uma camada inteligente que equilibra a responsividade com a completude das atualizações através de prioridades diferenciadas. As APIs como useTransition e useDeferredValue permitem que você comunique ao React qual trabalho pode ser adiado, resultando em interfaces que permanecem responsivas mesmo durante operações pesadas.
O Time Slicing transforma renderizações bloqueantes em fatias pequenas e não-bloqueantes, permitindo que interações do usuário sejam processadas rapidamente sem perder a capacidade de atualizar grandes quantidades de dados. Isso é crítico para aplicações modernas que lidam com grandes listas, gráficos complexos ou dados em tempo real. A chave é reconhecer quando aplicar essas técnicas e usar as ferramentas de profiling para validar que suas otimizações estão realmente melhorando a experiência do usuário.
Referências
-
React Documentation: Scheduler — Documentação oficial do Scheduler do React no repositório de código-fonte
-
React Docs: useTransition — Documentação oficial da API useTransition
-
React Docs: useDeferredValue — Documentação oficial da API useDeferredValue
-
Concurrent Features in React — Guia completo sobre renderização concorrente
-
Web Vitals and React Performance — Métricas de performance em navegadores Web