O atraso oculto de 300 ms do React.lazy e do Suspense
Aprofundamento CEVIU
Aprofundamento
A Tab Widgets é um app React 19 compilado com Rspack. Cada widget (relógio, notas, clima, Gmail, Spotify e dezenas de outros) vive em um chunk próprio carregado com React.lazy, e cada um fica dentro de sua própria fronteira de Suspense. Como uma extensão de nova aba começa do zero a cada abertura, a equipe achou que o bundle estava grande demais. Não estava. No Chromium headless, todo o código era carregado e avaliado em cerca de 100 ms, mas os widgets só apareciam por volta de 380 ms. Na maior parte desse intervalo, o navegador ficava ocioso.
O motivo são dois comportamentos do React que se combinam mal na inicialização. Um componente lazy sempre suspende na primeira renderização, mesmo com o código já carregado, porque o import() é chamado só nesse momento e a resposta vem por uma promise. Além disso, o React limita a revelação de conteúdo suspenso a uma vez a cada 300 ms (FALLBACK_THROTTLE_MS no código-fonte). Chamar import() antes não resolve, e envolver a renderização em startTransition também não, já que uma fronteira montando pela primeira vez ainda mostra o fallback. A primeira solução foi pré-carregar via component._init(component._payload), campos privados, e esperar até 100 ms antes de renderizar. Levou os widgets de 380 ms para 95 ms no headless, mas widgets mais lentos que o limite ainda suspendiam e apareciam até 100 ms mais tarde.
A correção definitiva foi não suspender. Cada widget verifica se o código já carregou. Se não, renderiza o placeholder como conteúdo comum e inicia o carregamento. Quando o código chega, um update de estado normal troca pelo widget real. No Chrome 154 no Windows 11, com um dashboard de 20 widgets, o primeiro conteúdo apareceu cerca de 240 ms antes e a maior parte do dashboard cerca de 200 ms antes. Tirar o luxon do bundle principal de 283 KB não mudou nada de mensurável. Tirar do startup um re-export que embutia os modais economizou cerca de 100 KB. Houve uma regressão: o spinner de carregamento dos modais roubava o foco, e os testes end-to-end pegaram. A solução foi pré-carregar os modais quando a página fica ociosa. Weather e Word of the Day ainda suspendem ao buscar dados, então continuam sujeitos ao throttle.
Por que isso importa
Para quem trabalha com React e code splitting, a leitura é que o tamanho do bundle nem sempre explica a lentidão percebida. Se o conteúdo fica pronto quase na hora, o throttle de 300 ms pode ser o maior atraso da tela. Vale medir quando as coisas aparecem, e não só quanto pesam os arquivos. A troca tem custo: os widgets surgem um a um, e não em lote, o que só funciona bem com molduras de tamanho fixo. O próprio texto reconhece o risco de depender de campos privados do React e mantém um teste de unidade para detectar quebras em upgrades.
Perguntas frequentes
Por que o React.lazy sempre suspende na primeira renderização?
O React.lazy só chama import() quando o componente renderiza pela primeira vez e recebe o resultado por uma promise, que sempre resolve depois dessa renderização. Por isso o primeiro render mostra o fallback do Suspense, mesmo que o código já esteja carregado. Chamar import() antes por conta própria não evita isso.
O que é o throttle de 300 ms do Suspense?
Quando o conteúdo suspenso fica pronto, o React só o revela depois que passaram 300 ms desde que um fallback apareceu ou foi substituído em qualquer ponto da página (FALLBACK_THROTTLE_MS). Tudo que ficar pronto nessa janela aparece junto. A intenção é evitar flicker, e há uma discussão no GitHub sobre torná-lo configurável, mas os mantenedores o mantêm como heurística fixa.
startTransition resolve esse atraso?
Não neste caso. Uma transição impede que conteúdo já visível volte a um fallback, mas uma fronteira de Suspense montando pela primeira vez ainda mostra o fallback. No build de desenvolvimento do React, a transição pode pular o fallback se o import já tiver resolvido, então o teste deve ser feito em build de produção.
Dá para fazer isso sem usar campos privados do React?
Dá. Em vez de React.lazy, você mantém um cache próprio de componentes carregados, inicia o import cedo, renderiza um placeholder até ele chegar e troca por um update de estado normal. Uma versão real precisa tratar falhas de import, por exemplo guardando o erro e lançando-o na renderização para um error boundary. Diferente do React.lazy, esse cache pode tentar de novo.
Fontes
- tabwidgets.comfonte original
- Categoria
- CEVIU Web Dev
- Publicado
- 06 de novembro de 2026
- Editoria
- CEVIU Web Dev

