Exibir códigos modernos em navegadores modernos para acelerar o carregamento de página

Neste codelab, melhore o desempenho deste aplicativo simples que permite que os usuários avaliem gatos aleatórios. Aprenda a otimizar o pacote JavaScript minimizando a quantidade de código transcompilado.

Captura de tela do aplicativo

No app de exemplo, é possível selecionar uma palavra ou emoji para transmitir o quanto você gosta de cada gato. Quando você clica em um botão, o app mostra o valor do botão abaixo da imagem do gato atual.

Medir

É sempre recomendável começar inspecionando um site antes de adicionar otimizações:

  1. Para visualizar o site, pressione View App. Em seguida, pressione Fullscreen tela cheia.
  2. Pressione "Control+Shift+J" (ou "Command+Option+J" no Mac) para abrir as Ferramentas do desenvolvedor.
  3. Clique na guia Rede.
  4. Marque a caixa de seleção Desativar cache.
  5. Atualize o app.

Solicitação de tamanho do pacote original

Mais de 80 KB são usados para este aplicativo. É hora de descobrir se partes do pacote não estão sendo usadas:

  1. Pressione Control+Shift+P (ou Command+Shift+P no Mac) para abrir o menu Command. Menu de comando

  2. Digite Show Coverage e pressione Enter para mostrar a guia Cobertura.

  3. Na guia Cobertura, clique em Recarregar para recarregar o aplicativo enquanto captura a cobertura.

    Recarregar o app com cobertura de código

  4. Confira quanto código foi usado em comparação com a quantidade carregada para o pacote principal:

    Cobertura de código do pacote

Mais da metade do pacote (44 KB) nem é utilizada. Isso ocorre porque grande parte 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. Novas versões 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? O Babel, que é incluído no aplicativo, é a biblioteca mais usada para compilar códigos que contêm 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 ES2015+ mais recentes, de modo que as APIs possam ser usadas mesmo que não tenham suporte do navegador. Confira um exemplo de polyfill do método Array.includes.
  • Os plug-ins são usados para transformar o código ES2015 (ou mais recente) em uma sintaxe ES5 mais antiga. Como essas são mudanças relacionadas à sintaxe (como funções de seta), elas não podem ser emuladas 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. Com isso, todas as configurações do Babel são definidas em um .babelrc na raiz do projeto.
  • 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 ECMAScript mais recentes para que eles possam funcionar em ambientes que não oferecem suporte a eles. Ele já foi importado na parte de cima de src/index.js.
import "./style.css";
import "@babel/polyfill";
  • @babel/preset-env identifica quais transformações e polyfills são necessários para todos os navegadores ou ambientes escolhidos como destinos.

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ê usa um bundler de módulos diferente do webpack.

O atributo targets em .babelrc identifica quais navegadores estão sendo segmentados. @babel/preset-env se integra ao browserslist, o que significa que você pode encontrar uma lista completa de consultas compatíveis que podem ser usadas neste campo na documentação do browserslist.

O valor "last 2 versions" transpila o código no aplicativo para as duas últimas versões de cada navegador.

Depuração

Para ter uma visão completa de todos os destinos do Babel do navegador e 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.

Atualize 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.

Navegadores segmentados

Observe como os navegadores descontinuados, como o Internet Explorer, estão incluídos nesta lista. Isso é um problema porque os navegadores sem suporte não terão recursos mais recentes adicionados, e o Babel continuará transpilando sintaxe específica 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:

Lista de plug-ins usados

Essa lista é bem longa. Esses são todos os plug-ins que o Babel precisa usar para transformar qualquer sintaxe ES2015+ em uma sintaxe mais antiga para todos os navegadores de destino.

No entanto, o Babel não mostra nenhum polyfill específico que é usado:

Nenhum polyfill foi adicionado

Isso acontece porque o @babel/polyfill inteiro 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:

Lista de polyfills importados

Embora apenas os polyfills necessários para "last 2 versions" sejam incluídos agora, a lista ainda é muito longa. Isso acontece porque os polyfills necessários para os navegadores de destino para cada recurso mais recente ainda são incluídos. Mude o valor do atributo para usage para incluir apenas os necessários para os recursos que estão sendo usados no código.

{
  "presets": [
    [
      "@babel/preset-env",
      {
        "targets": "last 2 versions",
        "debug": true,
        "useBuiltIns": "entry"
        "useBuiltIns": "usage"
      }
    ]
  ]
}

Com isso, 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.

Lista de polifills incluídos automaticamente

O tamanho do pacote de aplicativos é reduzido significativamente.

Tamanho do pacote reduzido para 30,1 KB

Como 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.

Tamanho do pacote: 30,0 KB

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 do navegador (como ">0.25%") e excluir navegadores específicos que você tem certeza de que os usuários não estão usando. Leia o artigo "Last 2 versions" considered harmful de James Kyle para saber mais sobre o assunto.

Use <script type="module">

Ainda há espaço para melhorar. Embora um número de polyfills não usados tenha sido removido, muitos estão sendo enviados e não são necessários para alguns navegadores. Ao usar módulos, a 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 com suporte em 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á têm suporte em ambientes que oferecem suporte a módulos JavaScript (em vez de 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 funcione em navegadores mais recentes com suporte a módulos e que inclua um módulo que não é totalmente transpilado, mas tem um tamanho de arquivo menor
  • Uma versão que inclui um script maior e transpilado que funciona em qualquer navegador legado

Como usar módulos ES com o Babel

Para ter configurações de @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 legada 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 os 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 enviados 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"})
];

Da mesma forma, crie um objeto de configuração para o script do módulo abaixo, em que legacyConfig é 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 aqui é 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 que é gerado neste 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á têm suporte em navegadores que oferecem suporte a módulos.

No final do arquivo, exporte as duas configurações em uma única matriz.

module.exports = [
  legacyConfig, moduleConfig
];

Agora, isso cria um módulo menor para navegadores compatíveis e um script transpilado maior para navegadores mais antigos.

Os navegadores que oferecem suporte a módulos ignoram scripts com um atributo nomodule. Por outro lado, os navegadores que não oferecem suporte a módulos ignoram elementos de script com type="module". Isso significa que você pode incluir um módulo e um substituto compilado. O ideal é que as duas versões do aplicativo estejam em index.html como esta:

<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 oposto.

É importante observar que, ao contrário dos scripts normais, 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 de cima 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 esse 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 do plug-in adicionam um atributo type="module" a todos os elementos de script .mjs, além de um atributo nomodule para todos os módulos de script .js.

Como exibir módulos no documento HTML

A última coisa que precisa 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 oferece suporte à saída dos scripts do módulo e do 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 que ofereça suporte a módulos, como a versão mais recente do Chrome.

Módulo de 5,2 KB buscado pela rede para navegadores mais recentes

Somente o módulo é buscado, com um tamanho de pacote muito menor, já que ele não é muito 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ão buscados. Confira uma captura de tela de todas as solicitações feitas em uma versão mais antiga do Chrome (versão 38).

Script de 30 KB buscado para navegadores mais antigos

Conclusão

Agora você entende como usar @babel/preset-env para fornecer apenas os polyfills necessários para os navegadores de destino. Você também sabe como os módulos JavaScript podem melhorar ainda mais o desempenho enviando 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!