Neste codelab, você vai melhorar o desempenho desse aplicativo simples que permite aos usuários classificar gatos aleatórios. Aprenda a otimizar o pacote JavaScript minimizando a quantidade de código transpilado.
No app de exemplo, você pode selecionar uma palavra ou um emoji para transmitir o quanto gosta de cada gato. Ao clicar em um botão, o app mostra o valor dele abaixo da imagem do gato atual.
Medir
É sempre bom começar inspecionando um site antes de adicionar otimizações:
- Para visualizar o site, pressione Ver app e depois Tela cheia
.
- Pressione "Control+Shift+J" (ou "Command+Option+J" no Mac) para abrir o DevTools.
- Clique na guia Rede.
- Marque a caixa de seleção Desativar cache.
- Recarregue o app.
Mais de 80 KB são usados para este aplicativo. É hora de descobrir se partes do pacote não estão sendo usadas:
Pressione
Control+Shift+P
(ouCommand+Shift+P
no Mac) para abrir o menu Comando.Digite
Show Coverage
e pressioneEnter
para mostrar a guia Cobertura.Na guia Cobertura, clique em Atualizar para recarregar o aplicativo enquanto captura a cobertura.
Confira quanto código foi usado em comparação com o que foi carregado para o pacote principal:
Mais da metade do pacote (44 KB) nem é utilizada. Isso acontece porque muito do código consiste em polyfills para garantir que o aplicativo funcione em navegadores mais antigos.
Usar @babel/preset-env
A sintaxe da linguagem JavaScript está em conformidade com um padrão conhecido como ECMAScript ou ECMA-262. Versões mais recentes da especificação são lançadas todos os anos e incluem novos recursos que passaram pelo processo de proposta. Cada navegador principal está sempre em um estágio diferente de suporte a esses recursos.
Os seguintes recursos do ES2015 são usados no aplicativo:
O seguinte recurso do ES2017 também é usado:
Confira o código-fonte em src/index.js
para saber como tudo isso é usado.
Todos esses recursos são compatíveis com a versão mais recente do Chrome, mas e outros navegadores que não são compatíveis? O Babel, incluído no aplicativo, é a biblioteca mais usada para compilar código com sintaxe mais recente em código que navegadores e ambientes mais antigos podem entender. Isso acontece de duas maneiras:
- Os polyfills são incluídos para emular funções mais recentes do ES2015+, de modo que as APIs possam ser usadas mesmo que não sejam compatíveis com o navegador. Confira um exemplo de um
polyfill
do método
Array.includes
. - Os plug-ins são usados para transformar o código ES2015 (ou posterior) em uma sintaxe ES5 mais antiga. Como essas são mudanças relacionadas à sintaxe (como funções de seta), não é possível emulá-las com polyfills.
Consulte package.json
para saber quais bibliotecas do Babel estão incluídas:
"dependencies": {
"@babel/polyfill": "^7.0.0"
},
"devDependencies": {
//...
"babel-loader": "^8.0.2",
"@babel/core": "^7.1.0",
"@babel/preset-env": "^7.1.0",
//...
}
@babel/core
é o compilador principal do Babel. Assim, todas as configurações do Babel são definidas em um.babelrc
na raiz do projeto.- O
babel-loader
inclui o Babel no processo de build do webpack.
Agora observe webpack.config.js
para ver como babel-loader
é incluído como uma
regra:
module: { rules: [ //... { test: /\.js$/, exclude: /node_modules/, loader: "babel-loader" } ] },
- O
@babel/polyfill
fornece todos os polyfills necessários para recursos mais recentes do ECMAScript, para que eles possam funcionar em ambientes que não os oferecem suporte. Ele já está importado na parte de cima desrc/index.js.
import "./style.css";
import "@babel/polyfill";
@babel/preset-env
identifica quais transformações e polyfills são necessários para qualquer navegador ou ambiente escolhido como destino.
Confira o arquivo de configurações do Babel, .babelrc
, para saber como ele é
incluído:
{
"presets": [
[
"@babel/preset-env",
{
"targets": "last 2 versions"
}
]
]
}
Esta é uma configuração do Babel e do webpack. Saiba como incluir o Babel no seu aplicativo se você usar um bundler de módulo diferente do webpack.
O atributo targets
em .babelrc
identifica os navegadores que estão sendo segmentados. O @babel/preset-env
se integra ao browserslist. Isso significa que você pode encontrar uma lista completa de consultas compatíveis que podem ser usadas nesse campo na documentação do browserslist.
O valor "last 2 versions"
transcompila o código no aplicativo para as duas últimas versões de cada navegador.
Depuração
Para ter uma visão completa de todas as metas do Babel do navegador, bem como de todas as transformações e polyfills incluídos, adicione um campo debug
a .babelrc:
.
{
"presets": [
[
"@babel/preset-env",
{
"targets": "last 2 versions",
"debug": true
}
]
]
}
- Clique em Ferramentas.
- Clique em Registros.
Recarregue o aplicativo e confira os registros de status do Glitch na parte de baixo do editor.
Navegadores segmentados
O Babel registra vários detalhes no console sobre o processo de compilação, incluindo todos os ambientes de destino para os quais o código foi compilado.
Observe como os navegadores descontinuados, como o Internet Explorer, estão incluídos nessa lista. Isso é um problema porque os navegadores sem suporte não recebem novos recursos, e o Babel continua transpilando sintaxes específicas para eles. Isso aumenta desnecessariamente o tamanho do pacote se os usuários não estiverem usando esse navegador para acessar seu site.
O Babel também registra uma lista de plug-ins de transformação usados:
É uma lista bem longa. Esses são todos os plug-ins que o Babel precisa usar para transformar qualquer sintaxe ES2015+ em sintaxe mais antiga para todos os navegadores segmentados.
No entanto, o Babel não mostra nenhum polyfill específico usado:
Isso acontece porque todo o @babel/polyfill
está sendo importado diretamente.
Carregar polyfills individualmente
Por padrão, o Babel inclui todos os polyfills necessários para um ambiente ES2015+ completo quando @babel/polyfill
é importado para um arquivo. Para importar polyfills específicos necessários para
os navegadores de destino, adicione um useBuiltIns: 'entry'
à configuração.
{
"presets": [
[
"@babel/preset-env",
{
"targets": "last 2 versions",
"debug": true
"useBuiltIns": "entry"
}
]
]
}
Recarregue o aplicativo. Agora você pode conferir todos os polyfills específicos incluídos:
Embora apenas os polyfills necessários para "last 2 versions"
sejam incluídos, ainda é uma lista muito longa. Isso acontece porque
os polyfills necessários para os navegadores de destino de todos os recursos mais recentes ainda estão incluídos. Mude o valor do atributo para usage
para incluir apenas os necessários para recursos que estão sendo usados no código.
{
"presets": [
[
"@babel/preset-env",
{
"targets": "last 2 versions",
"debug": true,
"useBuiltIns": "entry"
"useBuiltIns": "usage"
}
]
]
}
Assim, os polyfills são incluídos automaticamente quando necessário.
Isso significa que você pode remover a importação @babel/polyfill
em src/index.js.
.
import "./style.css";
import "@babel/polyfill";
Agora, apenas os polyfills necessários para o aplicativo são incluídos.
O tamanho do pacote de aplicativos é reduzido significativamente.
Restringir a lista de navegadores compatíveis
O número de destinos de navegador incluídos ainda é bastante grande, e poucos usuários usam navegadores descontinuados, como o Internet Explorer. Atualize as configurações para o seguinte:
{
"presets": [
[
"@babel/preset-env",
{
"targets": "last 2 versions",
"targets": [">0.25%", "not ie 11"],
"debug": true,
"useBuiltIns": "usage",
}
]
]
}
Confira os detalhes do pacote buscado.
Como o aplicativo é muito pequeno, não há muita diferença com essas mudanças. No entanto, a abordagem recomendada é usar uma porcentagem de participação de mercado de navegadores (como ">0.25%"
) e excluir navegadores específicos que você tem certeza de que seus usuários não estão usando. Leia o artigo "Last 2 versions" considered harmful (em inglês) de James Kyle para saber mais sobre isso.
Usar <script type="module">
Ainda há espaço para melhorias. Embora vários polyfills não utilizados tenham sido removidos, muitos estão sendo enviados e não são necessários para alguns navegadores. Ao usar módulos, uma sintaxe mais recente pode ser escrita e enviada diretamente aos navegadores sem o uso de polyfills desnecessários.
Os módulos JavaScript são um recurso relativamente novo compatível com todos os principais navegadores.
Os módulos podem ser criados usando um atributo type="module"
para definir scripts que importam e exportam de outros
módulos. Exemplo:
// math.mjs
export const add = (x, y) => x + y;
<!-- index.html -->
<script type="module">
import { add } from './math.mjs';
add(5, 2); // 7
</script>
Muitos recursos mais recentes do ECMAScript já são compatíveis com ambientes que aceitam módulos JavaScript, sem precisar do Babel. Isso significa que a configuração do Babel pode ser modificada para enviar duas versões diferentes do aplicativo ao navegador:
- Uma versão que funcionaria em navegadores mais recentes compatíveis com módulos e que inclui um módulo em grande parte não transcompilado, mas com um tamanho de arquivo menor.
- Uma versão que inclui um script maior e transpilado que funcionaria em qualquer navegador legado
Como usar módulos ES com Babel
Para ter configurações @babel/preset-env
separadas para as duas versões do
aplicativo, remova o arquivo .babelrc
. As configurações do Babel podem ser adicionadas à configuração do webpack especificando dois formatos de compilação diferentes para cada versão do aplicativo.
Comece adicionando uma configuração para o script legado a webpack.config.js
:
const legacyConfig = {
entry,
output: {
path: path.resolve(__dirname, "public"),
filename: "[name].bundle.js"
},
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
loader: "babel-loader",
options: {
presets: [
["@babel/preset-env", {
useBuiltIns: "usage",
targets: {
esmodules: false
}
}]
]
}
},
cssRule
]
},
plugins
}
Em vez de usar o valor targets
para "@babel/preset-env"
, esmodules
com um valor de false
é usado. Isso significa que o Babel
inclui todas as transformações e polyfills necessários para segmentar todos os navegadores que ainda não
oferecem suporte a módulos ES.
Adicione objetos entry
, cssRule
e corePlugins
ao início do arquivo webpack.config.js
. Todos eles são compartilhados entre o módulo e os scripts legados veiculados ao navegador.
const entry = {
main: "./src"
};
const cssRule = {
test: /\.css$/,
use: ExtractTextPlugin.extract({
fallback: "style-loader",
use: "css-loader"
})
};
const plugins = [
new ExtractTextPlugin({filename: "[name].css", allChunks: true}),
new HtmlWebpackPlugin({template: "./src/index.html"})
];
Agora, da mesma forma, crie um objeto de configuração para o script do módulo abaixo, em que legacyConfig
está definido:
const moduleConfig = {
entry,
output: {
path: path.resolve(__dirname, "public"),
filename: "[name].mjs"
},
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
loader: "babel-loader",
options: {
presets: [
["@babel/preset-env", {
useBuiltIns: "usage",
targets: {
esmodules: true
}
}]
]
}
},
cssRule
]
},
plugins
}
A principal diferença é que uma extensão de arquivo .mjs
é usada para o nome do arquivo de saída. O valor esmodules
é definido como "true" aqui, o que significa que o código
gerado nesse módulo é um script menor e menos compilado que não passa
por nenhuma transformação neste exemplo, já que todos os recursos usados
já são compatíveis com navegadores que oferecem suporte a módulos.
No final do arquivo, exporte as duas configurações em uma única matriz.
module.exports = [
legacyConfig, moduleConfig
];
Isso cria um módulo menor para navegadores compatíveis e um script transpilado maior para navegadores mais antigos.
Navegadores compatíveis com módulos ignoram scripts com um atributo nomodule
.
Por outro lado, os navegadores que não são compatíveis com módulos ignoram elementos de script com
type="module"
. Isso significa que você pode incluir um módulo e um
fallback compilado. O ideal é que as duas versões do aplicativo estejam em index.html
assim:
<script type="module" src="main.mjs"></script>
<script nomodule src="main.bundle.js"></script>
Os navegadores que oferecem suporte a módulos buscam e executam main.mjs
e ignoram main.bundle.js.
. Os navegadores que não oferecem suporte a módulos fazem o contrário.
É importante observar que, ao contrário dos scripts regulares, os scripts de módulo são sempre adiados por padrão.
Se você quiser que o script nomodule
equivalente também seja adiado e executado somente após
a análise, adicione o atributo defer
:
<script type="module" src="main.mjs"></script>
<script nomodule src="main.bundle.js" defer></script>
A última coisa que precisa ser feita aqui é adicionar os atributos module
e nomodule
ao módulo e ao script legado, respectivamente. Importe o ScriptExtHtmlWebpackPlugin na parte superior de webpack.config.js
:
const path = require("path");
const webpack = require("webpack");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const ScriptExtHtmlWebpackPlugin = require("script-ext-html-webpack-plugin");
Agora atualize a matriz plugins
nas configurações para incluir este plug-in:
const plugins = [ new ExtractTextPlugin({filename: "[name].css", allChunks: true}), new HtmlWebpackPlugin({template: "./src/index.html"}), new ScriptExtHtmlWebpackPlugin({ module: /\.mjs$/, custom: [ { test: /\.js$/, attribute: 'nomodule', value: '' }, ] }) ];
Essas configurações de plug-in adicionam um atributo type="module"
a todos os elementos de script .mjs
e um atributo nomodule
a todos os módulos de script .js
.
Como veicular módulos no documento HTML
A última coisa a ser feita é gerar os elementos de script legados e modernos no arquivo HTML. Infelizmente, o plug-in que cria o arquivo HTML final, HTMLWebpackPlugin
, não é compatível com a saída dos scripts de módulo e nomodule. Embora existam soluções alternativas e plug-ins separados criados para resolver esse problema, como BabelMultiTargetPlugin e HTMLWebpackMultiBuildPlugin, uma abordagem mais simples de adicionar o elemento de script do módulo manualmente é usada para fins deste tutorial.
Adicione o seguinte a src/index.js
no final do arquivo:
...
</form>
<script type="module" src="main.mjs"></script>
</body>
</html>
Agora carregue o aplicativo em um navegador compatível com módulos, como a versão mais recente do Chrome.
Somente o módulo é buscado, com um tamanho de pacote muito menor devido ao fato de ser em grande parte não transpilado. O outro elemento de script é completamente ignorado pelo navegador.
Se você carregar o aplicativo em um navegador mais antigo, apenas o script maior e transpilado com todos os polyfills e transformações necessários será buscado. Confira uma captura de tela de todas as solicitações feitas em uma versão mais antiga do Chrome (versão 38).
Conclusão
Agora você entende como usar @babel/preset-env
para fornecer apenas os polyfills necessários para navegadores específicos. Você também sabe como os módulos JavaScript
podem melhorar ainda mais a performance ao enviar duas versões transpiladas diferentes de um
aplicativo. Com um bom entendimento de como essas duas técnicas podem reduzir significativamente o tamanho do pacote, vá em frente e otimize!