Os aplicativos da Web de hoje podem ficar muito grandes, especialmente a parte JavaScript deles. Em meados de 2018, o HTTP Archive coloca o tamanho médio de transferência de JavaScript em dispositivos móveis em aproximadamente 350 KB. E isso é só o tamanho da transferência. O JavaScript geralmente é compactado quando enviado pela rede, o que significa que a quantidade real de JavaScript será um pouco maior depois que o navegador o descompactar. Isso é importante porque, no que diz respeito ao processamento de recursos, a compactação é irrelevante. 900 KB de JavaScript descompactado ainda são 900 KB para o analisador e o compilador, mesmo que sejam cerca de 300 KB quando compactados.
O JavaScript é um recurso caro para processar. Ao contrário das imagens que só geram um tempo de decodificação relativamente trivial após o download, o JavaScript precisa ser analisado, compilado e executado. Byte por byte, isso torna o JavaScript mais caro do que outros tipos de recursos.
As melhorias são contínuas para melhorar a eficiência dos mecanismos de JavaScript, mas melhorar a performance do JavaScript é, como sempre, uma tarefa para os desenvolvedores.
Para isso, há técnicas para melhorar a performance do JavaScript. O divisão de código é uma dessas técnicas que melhora a performance ao particionar o JavaScript do aplicativo em partes e servir essas partes apenas para as rotas de um aplicativo que precisam delas.
Embora essa técnica funcione, ela não resolve um problema comum de aplicativos com uso intenso de JavaScript, que é a inclusão de código que nunca é usado. A agitação de árvores tenta resolver esse problema.
O que é tree shaking?
O tree shaking é uma forma de eliminação de código inoperante. O termo foi popularizado por Rollup, mas o conceito de eliminação de código morto existe há algum tempo. O conceito também encontrou compra no webpack, que é demonstrado neste artigo por meio de um app de exemplo.
O termo "tree shaking" vem do modelo mental do aplicativo e das dependências dele como uma estrutura em árvore. Cada nó na árvore representa uma dependência que oferece uma funcionalidade distinta para o app. Em apps modernos, essas dependências são incluídas por instruções import
estáticas, como esta:
// Import all the array utilities!
import arrayUtils from "array-utils";
Quando um app é novo, ele pode ter poucas dependências. Ele também usa a maioria das dependências adicionadas. No entanto, à medida que o app amadurece, mais dependências podem ser adicionadas. Para piorar as coisas, 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 sendo enviado com muito JavaScript não usado. O tree shaking resolve isso aproveitando a forma 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 esse exemplo de import
e o anterior é que, em vez de importar tudo do módulo "array-utils"
, que pode ser muito código, esse exemplo importa apenas partes específicas dele. Em builds de desenvolvimento, isso não muda nada, já que todo o módulo é importado. Em builds de produção, o webpack pode ser configurado para "sacudir" as exportações de módulos ES6 que não foram explicitamente importados, tornando esses builds de produção menores. Neste guia, você vai aprender a fazer exatamente isso.
Encontrar oportunidades para sacudir uma árvore
Para fins ilustrativos, um app de exemplo de uma página está disponível para demonstrar como o recurso funciona. Você pode clonar e acompanhar se quiser, mas abordaremos cada etapa do caminho juntos neste guia, portanto, a clonagem não é necessária (a menos que você goste de aprender na prática).
O app de exemplo é um banco de dados pesquisável de pedais de efeitos para guitarra. Você insere uma consulta e uma lista de pedais de efeito é exibida.
O comportamento que impulsiona esse app é separado por fornecedor (ou seja, Preact e Emotion) e pacotes de código específicos do app (ou "chunks", como o webpack os chama):
Os pacotes JavaScript mostrados na figura acima são builds de produção, ou seja, eles são otimizados com a uglification. 21,1 KB para um pacote específico do app não é ruim, mas é importante observar que nenhum tremor de árvore está ocorrendo. Vamos analisar o código do app e ver o que pode ser feito para corrigir isso.
Em qualquer aplicativo, para encontrar oportunidades de tree shaking, é preciso procurar instruções import
estáticas. Próximo à parte superior do arquivo de componentes principal, você verá uma linha como esta:
import * as utils from "../../utils/utils";
É possível importar módulos ES6 de várias maneiras, mas exemplos como esse devem chamar sua atenção. Essa linha específica diz "import
tudo do módulo utils
e coloque em um namespace chamado utils
". A grande questão aqui é "quanto conteúdo há nesse módulo?"
Se você analisar o código-fonte do módulo utils
, vai encontrar cerca de 1.300 linhas de código.
Você precisa de tudo isso? Vamos verificar novamente pesquisando o arquivo principal do componente que importa o módulo utils
para saber quantas instâncias desse namespace aparecem.
O namespace utils
aparece em apenas três pontos do nosso aplicativo, mas para quais funções? Se você analisar o arquivo de componente principal novamente, ele parece ter apenas uma função, utils.simpleSort
, que é 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);
}
Em um arquivo de 1.300 linhas com várias exportações, apenas uma delas é usada. Isso resulta no envio de muito JavaScript não usado.
Embora esse exemplo de aplicativo seja um pouco inventivo, 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 aplicativo da Web de produção. Agora que você identificou uma oportunidade para o tree shaking ser útil, como isso pode ser feito?
Como impedir que o Babel transpile módulos ES6 em módulos CommonJS
O Babel é uma ferramenta indispensável, mas pode dificultar a observação dos efeitos da agitação da árvore. Se você estiver usando o @babel/preset-env
, o Babel pode transformar módulos ES6 em módulos CommonJS mais amplamente compatíveis, ou seja, módulos que você usa require
em vez de import
.
Como a redução de árvores é mais difícil de fazer para módulos CommonJS, o Webpack não saberá o que remover dos pacotes se você decidir usá-los. A solução é configurar @babel/preset-env
para deixar os módulos ES6 em paz. Em qualquer lugar que você configure 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 do @babel/preset-env
faz com que o Babel se comporte da forma desejada. Isso permite que o webpack analise a árvore de dependências e remova dependências não utilizadas.
Considerar os efeitos colaterais
Outro aspecto a ser considerado ao sacudir as 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 ao modificar a matriz fruits
, que está fora do escopo.
Os efeitos colaterais também se aplicam aos módulos ES6, e isso é importante no contexto do desmonte de árvore. 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 estiverem sendo usadas. Eles são partes de código modulares e independentes. Por isso, "módulos".
No caso do webpack, uma dica pode ser usada para especificar que um pacote e as dependências dele não têm efeitos colaterais. Para isso, basta especificar "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 têm efeitos colaterais:
{
"name": "webpack-tree-shaking-example",
"version": "1.0.0",
"sideEffects": [
"./src/utils/utils.js"
]
}
No último exemplo, qualquer arquivo não especificado será considerado livre de 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
.
Importar apenas o necessário
Depois de instruir o Babel a deixar os módulos ES6 em paz, um pequeno ajuste na sintaxe import
é necessário para trazer apenas as funções necessárias do módulo utils
. No exemplo deste guia, tudo o que é necessário é 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
precisam 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 que é necessário para que o tree shaking funcione neste exemplo. Esta é a saída do webpack antes de sacudir 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 tree shaking ser bem-sucedida:
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 encolhido, o pacote main
é o que se beneficia mais. Ao remover as partes não utilizadas do módulo utils
, o pacote main
é reduzido em cerca de 60%. Isso não apenas reduz o tempo que o script leva para fazer o download, mas também o tempo de processamento.
Vá sacudir algumas árvores!
O que você ganha com a eliminação de árvores depende do seu app, das dependências e da arquitetura dele. Confira! Se você sabe de fato que não configurou seu bundler de módulo para realizar essa otimização, não há problema em tentar e ver como isso beneficia seu aplicativo.
Você pode notar um aumento significativo no desempenho com a agitação da árvore ou quase nenhum. No entanto, ao configurar o sistema de build para aproveitar essa otimização nos builds de produção e importar seletivamente apenas o que o aplicativo precisa, você vai manter os pacotes do aplicativo o menor possível de forma proativa.
Um agradecimento especial a Kristofer Baxter, Jason Miller, Addy Osmani, Jeff Posnick, Sam Saccone e Philip Walton pelos comentários valiosos, que melhoraram significativamente a qualidade deste artigo.