Imports dinâmicos e code splitting
LidoNesta aula, você aprenderá a usar imports dinâmicos em JavaScript para carregar módulos sob demanda, melhorando a performance de aplicações web. Exploraremos a sintaxe import(), boas práticas de code splitting e o impacto real no tempo de carregamento.
Em aplicações JavaScript modernas, o tamanho do bundle é um dos principais fatores que afetam a performance. Carregar todo o código da aplicação de uma vez pode resultar em tempos de carregamento longos, especialmente em conexões lentas. Nesta aula, vamos explorar os imports dinâmicos e o code splitting como estratégias para otimizar o carregamento de módulos, carregando apenas o que é necessário no momento certo.
O import() é uma função que permite importar módulos de forma assíncrona, retornando uma Promise. Isso possibilita dividir o código em partes menores (chunks) que são carregadas somente quando necessárias, reduzindo o bundle inicial e melhorando a experiência do usuário.
import()
A sintaxe import() é uma forma dinâmica de importar módulos em JavaScript. Diferente do import estático, que deve ser usado no topo do arquivo e é resolvido em tempo de compilação, o import() pode ser chamado em qualquer lugar do código, como uma função comum, e retorna uma Promise que resolve para o módulo importado.
O uso típico é:
import('./modulo.js')
.then((modulo) => {
// usar o módulo
modulo.funcao();
})
.catch((erro) => {
console.error('Erro ao carregar módulo:', erro);
});Também é possível usar com async/await para um código mais limpo:
async function carregarModulo() {
try {
const modulo = await import('./modulo.js');
modulo.funcao();
} catch (erro) {
console.error('Erro ao carregar módulo:', erro);
}
}Uma característica importante é que o import() cria um novo escopo para o módulo, e as exportações são acessíveis como propriedades do objeto resolvido. Por exemplo, se o módulo exporta uma função padrão, você pode acessá-la via modulo.default.
Carregamento sob demanda
O carregamento sob demanda (lazy loading) é uma técnica que adia o carregamento de recursos até que eles sejam realmente necessários. No contexto de módulos JavaScript, isso significa que podemos dividir a aplicação em partes menores e carregá-las apenas quando o usuário realizar uma ação que exija esse código (como navegar para uma rota específica, abrir um modal, etc.).
Um exemplo clássico é o carregamento de uma biblioteca de gráficos somente quando o usuário clicar em um botão para exibir um gráfico:
const botao = document.getElementById('mostrar-grafico');
botao.addEventListener('click', async () => {
const { criarGrafico } = await import('./grafico.js');
criarGrafico();
});Em frameworks como React, é comum usar React.lazy() e Suspense para carregar componentes dinamicamente. Embora sejam específicos do React, eles utilizam o mesmo princípio de import() por baixo dos panos.
Outra aplicação é em rotas: em uma SPA, você pode carregar o código de cada rota somente quando o usuário navegar para ela. Isso reduz drasticamente o bundle inicial, pois o código de rotas não visitadas não é carregado.
Impacto em performance
O principal impacto positivo do code splitting é a redução do tempo de carregamento inicial. Ao dividir o bundle em chunks menores, o navegador precisa baixar menos bytes para exibir a primeira tela, o que melhora métricas como First Contentful Paint (FCP) e Largest Contentful Paint (LCP).
Além disso, o carregamento sob demanda pode reduzir o uso de memória, pois módulos que não são usados não são carregados na memória até serem necessários. Isso é especialmente útil em aplicações grandes com muitas funcionalidades.
No entanto, é preciso ter cuidado com o excesso de divisão: muitos chunks pequenos podem gerar muitas requisições HTTP, o que pode ter um impacto negativo. O ideal é encontrar um equilíbrio, criando chunks significativos que agrupem funcionalidades relacionadas. Ferramentas como webpack e Vite oferecem configurações para otimizar o code splitting automaticamente.
Outro ponto é o cache: chunks com nomes baseados em hash permitem que o navegador faça cache de partes que não mudaram, agilizando visitas subsequentes. Se um chunk é atualizado, apenas ele é rebaixado, enquanto os outros permanecem em cache.
Em termos de métricas, o code splitting pode melhorar significativamente o Lighthouse Performance Score, especialmente em sites com muitos scripts. No entanto, é importante monitorar o impacto real e ajustar a estratégia conforme necessário.
Boas práticas
Aqui estão algumas boas práticas ao usar imports dinâmicos:
- Use para funcionalidades pesadas e não críticas: bibliotecas de terceiros, componentes de UI complexos, gráficos, etc.
- Combine com
React.lazy(em React) ou similar: para componentes que só aparecem em certas condições. - Evite imports dinâmicos excessivos em loops: isso pode causar múltiplas requisições. Em vez disso, carregue uma vez e reutilize.
- Defina nomes de chunks significativos: use comentários mágicos em webpack (ex.:
/* webpackChunkName: "meu-chunk" */) para ajudar na depuração. - Trate erros de carregamento: sempre use
catchpara lidar com falhas de rede ou módulos ausentes.
Referências
- MDN: import()
- Webpack: Code Splitting
- Vite: Dynamic Import
- React: React.lazy
- MDN: Guia de Módulos JavaScript
- web.dev: Reduza o payload de JavaScript com code splitting
Exercícios
- Exercício 1: Escreva uma função
carregarModuloque useimport()para carregar um módulo chamado./utils.jse chame uma funçãosomarexportada por ele, com dois números fornecidos como argumentos. Useasync/awaite trate erros comtry/catch.✓ Resposta:async function carregarModulo(a, b) { try { const modulo = await import('./utils.js'); return modulo.somar(a, b); } catch (erro) { console.error('Erro ao carregar módulo:', erro); return null; } } - Exercício 2: Crie um exemplo de carregamento sob demanda: um botão que, ao ser clicado, carrega dinamicamente um módulo que exibe um alerta com "Módulo carregado!". Use
import()dentro do evento de clique.✓ Resposta:const botao = document.getElementById('meu-botao'); botao.addEventListener('click', async () => { try { const { mostrarAlerta } = await import('./alerta.js'); mostrarAlerta('Módulo carregado!'); } catch (erro) { console.error('Falha no carregamento:', erro); } }); - Exercício 3: Explique a diferença entre import estático e import dinâmico em relação ao tempo de carregamento e à possibilidade de uso condicional. Dê um exemplo de situação em que o import dinâmico é mais adequado.✓ Resposta: O import estático é resolvido em tempo de compilação e carregado junto com o bundle principal, aumentando o tamanho inicial. O import dinâmico é carregado em tempo de execução, apenas quando a chamada é feita, permitindo carregamento condicional e sob demanda. Um exemplo adequado é carregar uma biblioteca de gráficos somente quando o usuário clicar em um botão para gerar um gráfico, evitando carregar esse código desnecessariamente na carga inicial.
- Exercício 4: Suponha que você tem um módulo que exporta uma função
calcular. Como você importaria esse módulo dinamicamente e usaria a função em uma expressãoawait? Mostre o código completo, incluindo o tratamento de erro.✓ Resposta:async function executarCalculo(a, b) { try { const { calcular } = await import('./calculadora.js'); return await calcular(a, b); } catch (erro) { console.error('Erro ao carregar ou executar:', erro); return null; } } - Exercício 5: Cite duas métricas de performance que podem ser melhoradas com code splitting e explique como o carregamento dinâmico contribui para cada uma delas.✓ Resposta: Duas métricas são First Contentful Paint (FCP) e Time to Interactive (TTI). O code splitting reduz o tamanho do bundle inicial, permitindo que o navegador baixe e execute menos JavaScript antes de renderizar o conteúdo, melhorando o FCP. Além disso, ao adiar a execução de scripts não essenciais, o thread principal fica livre mais cedo, reduzindo o TTI.