Você tem um menu sólido. Os clientes gostam. Funciona bem. Mas toda vez que testa sua loja no PageSpeed Insights, o Google sinaliza um arquivo específico: seu script de menu. O rótulo diz “render-blocking JavaScript”, o arquivo tem 120KB e está adicionando 600 milissegundos ao seu LCP móvel. Você sabe que precisa corrigir, mas a última vez que alguém sugeriu usar defer, o menu quebrou. Metade dos dropdowns parou de funcionar, o toggle móvel não fez nada, e você voltou atrás em uma hora.
Este é um problema comum. Scripts que bloqueiam renderização são um dos gargalos de desempenho mais impactantes em lojas Shopify, e o menu é frequentemente o culpado. Mas diferir JavaScript não é apenas adicionar um atributo a uma tag de script. Se você fizer errado, o menu não vai funcionar. Se fizer certo, pode tirar meio segundo do seu LCP móvel sem tocar em uma única linha de lógica do menu.
Este artigo explica o que render-blocking realmente significa, por que async e defer se comportam diferentemente, e como carregar seu script de menu sem quebrar a funcionalidade.
- Scripts que bloqueiam renderização pausam toda renderização de página até serem baixados, processados e executados.
deferbaixa scripts em paralelo, mas adia a execução até que o parse do HTML seja concluído.asyncbaixa e executa imediatamente, o que pode quebrar menus que dependem da ordem do DOM.type="module"adia por padrão e suporta recursos modernos de JavaScript.- A maioria das quebras de menu após usar defer é causada por timing de inicialização, não pelo atributo defer em si.
O Que Render-Blocking Realmente Significa
Quando um navegador encontra uma tag <script> no HTML sem atributos de carregamento, ele para tudo. Pausa o parse do HTML, baixa o script, faz parse, executa, e só depois continua construindo a página. Isso é chamado comportamento síncrono ou render-blocking.
A lógica por trás desse comportamento é antiga, mas razoável: JavaScript pode modificar o DOM usando document.write(), então o navegador assume que todo script pode mudar a estrutura da página e portanto deve rodar antes de prosseguir.
Aqui está o problema. Seu script de menu não usa document.write(). Não precisa rodar antes do resto da página ser processada. Mas o navegador não sabe disso, então bloqueia mesmo assim.
Segundo a própria documentação do Chrome sobre como otimizar o Largest Contentful Paint, eliminar JavaScript que bloqueia renderização é uma das melhorias de mais alto impacto que você pode fazer. Quando um script no head do documento bloqueia por 400 milissegundos, cada elemento abaixo dele no DOM — sua imagem hero, seu headline, sua grade de produtos — espera 400 milissegundos a mais para renderizar.
As Três Estratégias de Carregamento: Padrão, Defer e Async
Existem três formas de um navegador carregar um script.
Padrão (Síncrono)
<script src="menu.js"></script>
O navegador para o parse do HTML, baixa menu.js, faz parse, executa, depois retoma. Isso bloqueia a renderização por toda a duração.
Defer
<script src="menu.js" defer></script>
O navegador baixa menu.js em paralelo com o parse do HTML, mas não executa até que todo o documento HTML tenha sido processado. Múltiplos scripts com defer executam na ordem em que aparecem no HTML.
Async
<script src="menu.js" async></script>
O navegador baixa menu.js em paralelo com o parse do HTML e executa assim que o download é concluído, mesmo que o parse do HTML não tenha terminado. A ordem de execução é imprevisível se múltiplos scripts usarem async.
Aqui está uma tabela resumindo o comportamento:
| Atributo | Download | Execução | Bloqueia Parse do HTML | Ordem de Execução |
|---|---|---|---|---|
| Nenhum (padrão) | Bloqueia parse | Imediata | Sim | Sequencial |
defer |
Paralelo | Depois do parse do HTML | Não | Sequencial |
async |
Paralelo | Assim que pronto | Sim (durante execução) | Imprevisível |
Para a maioria dos menus de navegação, defer é a escolha certa. Mantém scripts em ordem, o que importa quando um script depende de outro (por exemplo, um script de menu que depende de uma biblioteca utilitária). E garante que o DOM está pronto antes do script rodar, o que é crítico para qualquer código que consulta elementos.
Por Que Defer Quebra Alguns Menus (E Como Corrigir)
Você adiciona defer à tag do seu script de menu, recarrega a página, e o menu não funciona. Os dropdowns não abrem. O toggle móvel não faz nada. Você remove defer e tudo funciona novamente. O que aconteceu?
O problema é quase sempre o timing de inicialização. Aqui estão as três causas mais comuns.
Problema 1: Script Roda Antes do JavaScript do Tema Estar Pronto
Muitos temas Shopify têm um objeto JavaScript global ou função de inicialização que outros scripts dependem. Se seu script de menu rodar antes daquele objeto ser criado, ele falha.
Exemplo:
// theme.js (carregado primeiro)
window.Theme = { utils: { ... } };
// menu.js (carregado segundo, espera que Theme exista)
Theme.utils.initDropdown();
Se menu.js é deferido mas theme.js não, menu.js pode rodar antes de theme.js terminar de executar, e Theme será undefined.
Solução: Difera ambos os scripts, ou garanta que menu.js verifica dependências antes de inicializar.
if (typeof Theme !== 'undefined') {
Theme.utils.initDropdown();
}
Problema 2: Scripts Inline Rodam Antes de Scripts Deferidos
Scripts inline (JavaScript escrito direto no HTML, não carregado de um arquivo externo) executam em ordem de documento conforme são encontrados. Scripts deferidos executam após todo o HTML ser processado, o que significa depois de todos os scripts inline.
Se você tem um script inline que tenta inicializar o menu, e a biblioteca de menu é deferida, a inicialização vai falhar.
Solução: Mova a lógica de inicialização para dentro do script deferido, ou envolva a inicialização inline em um listener DOMContentLoaded.
document.addEventListener('DOMContentLoaded', function() {
if (typeof MenuApp !== 'undefined') {
MenuApp.init();
}
});
Problema 3: Race Conditions com Múltiplos Scripts Deferidos
Se seu menu depende de uma biblioteca utilitária, e ambos são deferidos, devem executar na ordem em que aparecem no HTML. Mas se a biblioteca utilitária é carregada assincronamente (usando async em vez de defer), a ordem de execução fica imprevisível.
Solução: Use defer para todos os scripts interdependentes, não async. Se você deve usar async, adicione verificações de dependência explícitas ou use importações dinâmicas.
Type=”module”: A Alternativa Moderna
JavaScript moderno suporta módulos ES6, que têm carregamento deferido integrado.
<script type="module" src="menu.js"></script>
Scripts com type="module" automaticamente deferrem. Baixam em paralelo e executam depois do parse do HTML ser concluído, assim como defer. Também suportam import e export, o que torna o gerenciamento de dependências explícito.
Se seu script de menu é escrito como um módulo, você pode importar dependências diretamente:
import { initDropdown } from './dropdown.js';
initDropdown();
Isso elimina os problemas de timing que causam quebras com defer tradicional, porque o navegador sabe exatamente quais scripts dependem de quais.
A desvantagem: navegadores antigos (Internet Explorer, versões muito antigas do Safari) não suportam módulos. Mas segundo o Can I Use, mais de 95% do tráfego global de navegadores suporta módulos ES6 a partir de 2025. Para a maioria das lojas Shopify, isso é seguro.
Considerações Específicas do Shopify
Temas Shopify carregam scripts de formas diferentes dependendo da arquitetura do tema. Aqui está como deferring funciona nos padrões mais comuns.
Tags de Script em Theme.liquid
Se seu script de menu é carregado em theme.liquid assim:
{{ 'menu.js' | asset_url | script_tag }}
O filtro script_tag não adiciona defer por padrão. Você precisa substituir com uma tag <script> manual:
<script src="{{ 'menu.js' | asset_url }}" defer></script>
Blocos App Embed
Se seu menu é instalado como um app embed (comum para aplicativos de menu de terceiros), o app controla como seu script é carregado. Você não pode adicionar defer você mesmo. Você precisa verificar com o desenvolvedor do app ou trocar para um app de menu que já carrega scripts assincronamente.
Ferramentas como Navi+ carregam seu JavaScript com defer por padrão, o que é uma razão pela qual têm melhor desempenho em testes LCP do que aplicativos de menu legados.
Renderização de Seção e Scripts Deferidos
Temas Shopify 2.0 carregam seções dinamicamente. Se uma seção inclui um script deferido, e a seção é adicionada à página após o carregamento inicial (por exemplo, via AJAX para quick view ou infinite scroll), o script pode não executar conforme esperado.
Solução: Use DOMContentLoaded apenas para carregamento inicial da página. Para seções adicionadas dinamicamente, dispare a inicialização manualmente usando um evento customizado ou mutation observer.
Como Testar Se Defer Funciona
Antes de implementar carregamento de script deferido em sua loja live, teste completamente. Aqui está um checklist.
- Chrome Desktop: Abra sua loja, abra DevTools, vá para a aba Network, recarregue, e verifique que o script de menu baixa em paralelo com outros recursos e não bloqueia o document.
- Simulação móvel: Em DevTools, mude para emulação móvel (iPhone ou Android), diminua a velocidade de rede para “Fast 3G”, e recarregue. O menu deve ainda inicializar corretamente.
- Entre navegadores: Teste em Safari, Firefox e Edge. Scripts de módulo se comportam um pouco diferentes no Safari.
- Dispositivo real: Abra a loja em um telefone real com uma conexão móvel real. Emulação é útil, mas latência do mundo real expõe bugs de timing que testes desktop perdem.
- Verificação de funcionalidade: Abra cada dropdown, teste o toggle do menu móvel, clique cada link, e verifique que nada quebra.
Antes de fazer pushExecute um teste do PageSpeed Insights na mesma página antes e depois de ativar defer. Você deve ver uma queda mensurável em LCP e uma redução em recursos que bloqueiam renderização. Se LCP não melhorar, o script pode não ter sido o gargalo, ou pode haver outros recursos bloqueantes para abordar.
Quando Async Faz Sentido (E Quando Não)
async é apropriado para scripts que são completamente auto-contidos e não dependem da ordem do DOM ou de outros scripts. Exemplos incluem trackers de análise, widgets de chat, e alguns scripts de anúncios.
Para menus de navegação, async é arriscado. O menu precisa que o DOM esteja pronto, frequentemente depende de utilitários do tema, e geralmente precisa rodar antes do cliente interagir com a página. Se o script executar enquanto o HTML ainda está sendo processado, pode tentar consultar elementos que não existem ainda.
Se você está considerando async para um script de menu, o script deve:
- Não depender de nenhum outro script
- Não consultar elementos do DOM (ou envolver todas as consultas em uma verificação
DOMContentLoaded) - Ser pequeno o bastante que seu tempo de execução não bloqueie o parse
A maioria dos scripts de menu não atende esses critérios. Fique com defer ou type="module" a menos que tenha uma razão específica para usar async.
Como Carregamento de Script de Menu Bom Se Parece
Um script de menu otimizado corretamente em uma loja Shopify se parece assim:
<script src="{{ 'menu.js' | asset_url }}" defer></script>
Ou, se usando módulos:
<script type="module" src="{{ 'menu.js' | asset_url }}"></script>
O script baixa em paralelo com o resto da página, executa depois que o DOM está pronto, e não bloqueia a imagem hero ou nenhum outro elemento LCP de renderizar.
Na timeline de Performance do Chrome DevTools, você deve ver a imagem hero renderizar antes do script de menu executar. Se o script de menu rodar primeiro, ainda está bloqueando.
A maioria das lojas pode cortar 300 a 600 milissegundos do LCP móvel apenas mudando de carregamento de script síncrono para deferido. O menu ainda funciona. O cliente não nota diferença. Mas o Google nota, e mais importante, o navegador nota. A página fica visível mais rápido, que é o ponto inteiro.
Este artigo é parte do guia maior sobre Menu e LCP: como a navegação bloqueia seu maior contentful paint.