Os aplicativos da web de hoje podem ficar muito grandes, especialmente a parte JavaScript deles. Em meados de 2018, o HTTP Archive colocou o tamanho médio de transferência do JavaScript em dispositivos móveis (link em inglês) de aproximadamente 350 KB. E isso é apenas o tamanho da transferência. O JavaScript geralmente é compactado quando enviado pela rede, o que significa que a quantidade real de JavaScript é um pouco maior depois que o navegador o descompacta. É importante ressaltar que, no que diz respeito ao processamento de recursos, a compactação é irrelevante. 900 KB de JavaScript descompactado ainda equivalem a 900 KB para o analisador e o compilador, embora possa ter aproximadamente 300 KB quando compactado.
JavaScript é um recurso caro para ser processado. Ao contrário das imagens, que só geram um tempo de decodificação relativamente trivial após o download, é preciso analisar, compilar e executar o JavaScript. Byte por byte, isso torna o JavaScript mais caro do que outros tipos de recursos.
Embora estejam sendo feitas melhorias continuamente para aumentar a eficiência dos mecanismos JavaScript, melhorar o desempenho do JavaScript é, como sempre, uma tarefa para os desenvolvedores.
Para isso, existem técnicas que melhoram o desempenho do JavaScript. A divisão de código é uma das técnicas que melhora o desempenho particionando o JavaScript do aplicativo em blocos e disponibilizando esses blocos apenas para as rotas de um aplicativo que precisam deles.
Embora essa técnica funcione, ela não aborda um problema comum de aplicativos com muitos JavaScript, que é a inclusão de código que nunca é usado. O tree shaking tenta resolver esse problema.
O que é tree shaking?
O tree shaking é uma forma de eliminação de códigos inativos. O termo foi popularizado pela Rollup, mas o conceito de eliminação de código morto já existia há algum tempo. Esse conceito também encontrou compra no webpack, o que é demonstrado neste artigo com um app de exemplo.
O termo "tree shaking" é proveniente do modelo mental do aplicativo e das dependências dele como uma estrutura semelhante a uma árvore. Cada nó da árvore representa uma dependência que oferece funcionalidades distintas ao app. Em apps modernos, essas dependências são trazidas por instruções import
estáticas, da seguinte forma:
// Import all the array utilities!
import arrayUtils from "array-utils";
Quando um app é jovem (por exemplo, uma muda, ele pode ter poucas dependências). Ele também está usando a maioria das dependências adicionadas, se não todas. No entanto, à medida que seu app amadurece, mais dependências podem ser adicionadas. Para isso, as dependências mais antigas deixam de ser usadas, mas podem não ser removidas da base de código. O resultado final é que um app acaba tendo muito JavaScript não utilizado. O tree shaking resolve isso aproveitando como as instruções import
estáticas extraem partes específicas dos módulos ES6:
// Import only some of the utilities!
import { unique, implode, explode } from "array-utils";
A diferença entre este exemplo de import
e o anterior é que, em vez de importar tudo do módulo "array-utils"
, o que pode ser muito código, ele importa apenas partes específicas dele. Em builds dev, isso não muda nada, já que o módulo inteiro é importado de qualquer maneira. Em builds de produção, o webpack pode ser configurado para "balançar" as exportações de módulos ES6 que não foram importados explicitamente, tornando essas versões de produção menores. Neste guia, você vai aprender a fazer exatamente isso.
Encontrar oportunidades para sacudir uma árvore
Para fins ilustrativos, disponibilizamos um exemplo de aplicativo de uma página que demonstra como funciona o tree shaking. Você pode cloná-la e acompanhar se quiser, mas abordaremos todas as etapas neste guia, portanto, a clonagem não é necessária (a menos que você aprenda na prática).
O app de exemplo é um banco de dados pesquisável dos pedais de efeito de guitarra. Você insere uma consulta e uma lista de pedais de efeito aparece.
O comportamento que impulsiona esse app é separado por fornecedor (ou seja, Preact e Emotion) e pacotes de código específicos do app (ou "blocos", como os chamados pelo webpack):
Os pacotes JavaScript mostrados na figura acima são builds de produção, o que significa que são otimizados por uglificação. 21,1 KB para um pacote específico de app não é ruim, mas não está acontecendo o tree shaking. Vamos analisar o código do app e descobrir o que pode ser feito para corrigir isso.
Em qualquer aplicativo, a busca por oportunidades de tree shaking envolve a busca por instruções import
estáticas. Perto da parte superior do arquivo do componente principal, você verá uma linha como esta:
import * as utils from "../../utils/utils";
É possível importar módulos ES6 de várias maneiras, mas coisas como essa podem chamar sua atenção. Essa linha específica diz "import
tudo do módulo utils
e coloque-o em um namespace chamado utils
". A grande pergunta a ser feita aqui é "quantas coisas tem nesse módulo?"
Você verá que há cerca de 1.300 linhas de código no código-fonte do módulo utils
.
Você precisa de tudo isso? Vamos verificar novamente pesquisando o arquivo de componente principal que importa o módulo utils
para ver quantas instâncias desse namespace são exibidas.
O namespace utils
aparece em apenas três pontos do aplicativo, mas para quais funções? Se você observar o arquivo do componente principal novamente, ele parece ser apenas uma função, utils.simpleSort
, usada para classificar a lista de resultados da pesquisa por vários critérios quando os menus suspensos de classificação são alterados:
if (this.state.sortBy === "model") {
// `simpleSort` gets used here...
json = utils.simpleSort(json, "model", this.state.sortOrder);
} else if (this.state.sortBy === "type") {
// ..and here...
json = utils.simpleSort(json, "type", this.state.sortOrder);
} else {
// ..and here.
json = utils.simpleSort(json, "manufacturer", this.state.sortOrder);
}
De um arquivo de 1.300 linhas com várias exportações, apenas uma delas é usada. Isso resulta no envio de muitos JavaScript não utilizados.
Embora esse app de exemplo seja um pouco irreal, ele não muda o fato de que esse tipo de cenário sintético se assemelha a oportunidades reais de otimização que você pode encontrar em um app da Web de produção. Agora que você identificou uma oportunidade para o tree shaking ser útil, como ele pode ser feito?
Como impedir que o Babel transcompile módulos ES6 em módulos CommonJS
O Babel é uma ferramenta indispensável, mas pode dificultar a observação dos efeitos de agitação das árvores. Se você usar @babel/preset-env
, o Babel pode transformar os módulos ES6 em módulos CommonJS mais amplamente compatíveis, ou seja, módulos que você require
em vez de import
.
Como o tree shaking é mais difícil de fazer nos módulos CommonJS, o webpack não sabe o que remover dos pacotes se você decidir usá-los. A solução é configurar @babel/preset-env
para deixar explicitamente os módulos ES6 sozinhos. Independentemente de onde você configurar o Babel, seja em babel.config.js
ou package.json
, isso envolve adicionar algo extra:
// babel.config.js
export default {
presets: [
[
"@babel/preset-env", {
modules: false
}
]
]
}
Especificar modules: false
na configuração @babel/preset-env
faz com que o Babel se comporte como desejado, o que permite que o webpack analise sua árvore de dependências e elimine as dependências não usadas.
Considerações para os efeitos colaterais
Outro aspecto a ser considerado ao agitar dependências do app é se os módulos do projeto têm efeitos colaterais. Um exemplo de efeito colateral é quando uma função modifica algo fora do próprio escopo, que é um efeito colateral da execução:
let fruits = ["apple", "orange", "pear"];
console.log(fruits); // (3) ["apple", "orange", "pear"]
const addFruit = function(fruit) {
fruits.push(fruit);
};
addFruit("kiwi");
console.log(fruits); // (4) ["apple", "orange", "pear", "kiwi"]
Neste exemplo, addFruit
produz um efeito colateral quando modifica a matriz fruits
, que está fora do escopo.
Os efeitos colaterais também se aplicam aos módulos ES6, o que é importante no contexto do tree shaking. Os módulos que recebem entradas previsíveis e produzem saídas igualmente previsíveis sem modificar nada fora do próprio escopo são dependências que podem ser descartadas com segurança se não forem usados. Elas são partes de código modulares e independentes. Portanto, "módulos".
Quando o webpack está relacionado, uma dica pode ser usada para especificar que um pacote e as dependências dele não têm efeitos colaterais, especificando "sideEffects": false
no arquivo package.json
de um projeto:
{
"name": "webpack-tree-shaking-example",
"version": "1.0.0",
"sideEffects": false
}
Como alternativa, você pode informar ao webpack quais arquivos específicos não estão livres de efeitos colaterais:
{
"name": "webpack-tree-shaking-example",
"version": "1.0.0",
"sideEffects": [
"./src/utils/utils.js"
]
}
No último exemplo, será considerado que qualquer arquivo não especificado não tenha efeitos colaterais. Se você não quiser adicionar isso ao arquivo package.json
, também é possível especificar essa flag na configuração do webpack usando module.rules
.
Você só precisa do que é necessário
Depois de instruir o Babel a deixar os módulos ES6 sozinhos, é necessário fazer um pequeno ajuste na sintaxe import
para trazer apenas as funções necessárias do módulo utils
. Neste exemplo do guia, só é necessário ter a função simpleSort
:
import { simpleSort } from "../../utils/utils";
Como apenas simpleSort
está sendo importado em vez de todo o módulo utils
, todas as instâncias de utils.simpleSort
precisarão ser alteradas para simpleSort
:
if (this.state.sortBy === "model") {
json = simpleSort(json, "model", this.state.sortOrder);
} else if (this.state.sortBy === "type") {
json = simpleSort(json, "type", this.state.sortOrder);
} else {
json = simpleSort(json, "manufacturer", this.state.sortOrder);
}
Isso é tudo o que é necessário para que o tree shaking funcione neste exemplo. Esta é a saída do webpack antes de balancear a árvore de dependências:
Asset Size Chunks Chunk Names
js/vendors.16262743.js 37.1 KiB 0 [emitted] vendors
js/main.797ebb8b.js 20.8 KiB 1 [emitted] main
Esta é a saída após a conclusão do tree shaking:
Asset Size Chunks Chunk Names
js/vendors.45ce9b64.js 36.9 KiB 0 [emitted] vendors
js/main.559652be.js 8.46 KiB 1 [emitted] main
Embora os dois pacotes tenham diminuído, é a main
que mais se beneficia. Ao absorver as partes não usadas do módulo utils
, o pacote main
é reduzido em cerca de 60%. Isso não apenas diminui o tempo que o script leva para o download, mas também o tempo de processamento.
Vai chacoalhar as árvores!
O impacto do tree shaking depende do seu app, das dependências e da arquitetura dele. Confira! Se você sabe mesmo que ainda não configurou o bundler de módulos para realizar essa otimização, não tem problema tentar e conferir os benefícios para seu aplicativo.
Talvez você perceba um ganho de desempenho significativo com o tree shaking ou não terá grandes oportunidades. No entanto, ao configurar o sistema de compilação para aproveitar essa otimização em builds de produção e importar seletivamente apenas o que o aplicativo precisa, você vai manter os pacotes de aplicativos os menores possíveis.
Agradecimentos especiais a Kristofer Baxter, Jason Miller, Addy Osmani, Jeff Posnick, Sam Saccone e Philip Walton pelo feedback valioso, que melhorou significativamente a qualidade deste artigo.