Publique, envie e instale o JavaScript moderno para aplicativos mais rápidos

Melhore o desempenho ativando a saída e as dependências modernas do JavaScript.

Mais de 90% dos navegadores são capazes de executar o JavaScript moderno, mas a predominância do JavaScript legado continua sendo uma grande fonte de problemas de desempenho na Web atualmente.

JavaScript moderno

O JavaScript moderno não se caracteriza como código escrito em uma versão específica da especificação ECMAScript, mas sim em sintaxe compatível com todos os navegadores modernos. Navegadores da Web modernos, como Chrome, Edge, Firefox e Safari representam mais de 90% do mercado de navegadores, e navegadores diferentes que dependem dos mesmos mecanismos de renderização subjacentes representam 5% a mais. Isso significa que 95% do tráfego da Web global vem de navegadores compatíveis com os recursos da linguagem JavaScript mais usados nos últimos 10 anos, incluindo:

  • Classes (ES2015)
  • Funções de seta (ES2015)
  • Geradores (ES2015)
  • Escopo de blocos (ES2015)
  • Desestruturação (ES2015)
  • Parâmetros de descanso e propagação (ES2015)
  • Abreviação de objetos (ES2015)
  • Async/await (ES2017)

Os recursos em versões mais recentes da especificação de linguagem geralmente têm suporte menos consistente em navegadores modernos. Por exemplo, muitos recursos ES2020 e ES2021 são compatíveis apenas com 70% do mercado de navegadores. Ainda é a maioria dos navegadores, mas não o suficiente para que seja seguro confiar nesses recursos diretamente. Isso significa que, embora o JavaScript "moderno" seja um destino móvel, o ES2017 tem a maior variedade de compatibilidade com navegadores, além de incluir a maioria dos recursos de sintaxe moderna mais usados. Em outras palavras, o ES2017 é a sintaxe moderna mais próxima da atual.

JavaScript legado

JavaScript legado é um código que evita especificamente o uso de todos os recursos de linguagem acima. A maioria dos desenvolvedores escreve o código-fonte usando sintaxe moderna, mas compila tudo para a sintaxe legada para aumentar o suporte ao navegador. Compilar com a sintaxe legada aumenta o suporte ao navegador, mas o efeito costuma ser menor do que imaginamos. Em muitos casos, o suporte aumenta de cerca de 95% para 98%, gerando um custo significativo:

  • Normalmente, o JavaScript legado é cerca de 20% maior e mais lento do que o código moderno equivalente. Deficiências de ferramentas e configurações incorretas muitas vezes aumentam essa lacuna ainda mais.

  • As bibliotecas instaladas representam até 90% do código JavaScript de produção típico. O código da biblioteca gera uma sobrecarga de JavaScript legado ainda maior devido ao polyfill e à duplicação de ajuda que podem ser evitados publicando códigos modernos.

JavaScript moderno no npm

Recentemente, o Node.js padronizou um campo "exports" para definir pontos de entrada para um pacote:

{
  "exports": "./index.js"
}

Os módulos referenciados pelo campo "exports" implicam uma versão do nó de no mínimo 12.8, compatível com ES2019. Isso significa que qualquer módulo referenciado usando o campo "exports" pode ser escrito em JavaScript moderno. Os consumidores de pacotes precisam supor que os módulos com um campo "exports" contêm um código moderno e transcompilam, se necessário.

Somente moderna

Se você quiser publicar um pacote com código moderno e deixar que o consumidor processe a transcompilação quando ele for usado como dependência, use apenas o campo "exports".

{
  "name": "foo",
  "exports": "./modern.js"
}

Moderna com substituto legado

Use o campo "exports" com "main" para publicar seu pacote usando código moderno, mas também inclua um substituto ES5 + CommonJS para navegadores legados.

{
  "name": "foo",
  "exports": "./modern.js",
  "main": "./legacy.cjs"
}

Moderno com substituto legado e otimizações do bundler ESM

Além de definir um ponto de entrada de fallback do CommonJS, o campo "module" pode ser usado para apontar para um pacote substituto legado semelhante, mas que usa a sintaxe de módulo JavaScript (import e export).

{
  "name": "foo",
  "exports": "./modern.js",
  "main": "./legacy.cjs",
  "module": "./module.js"
}

Muitos bundlers, como webpack e Rollup, dependem desse campo para aproveitar os recursos do módulo e ativar o tree shaking. Esse ainda é um pacote legado que não contém nenhum código moderno além da sintaxe import/export. Portanto, use essa abordagem para enviar códigos modernos com um substituto legado otimizado para pacotes.

JavaScript moderno em aplicativos

As dependências de terceiros compõem a grande maioria do código JavaScript de produção típico em aplicativos da Web. Embora as dependências do npm tenham sido publicadas como sintaxe ES5 legada, essa não é mais uma suposição segura e há o risco de que as atualizações de dependência afetem a compatibilidade com navegadores do aplicativo.

Com um número cada vez maior de pacotes npm migrando para o JavaScript moderno, é importante garantir que as ferramentas de build estejam configuradas para lidar com eles. É bem provável que alguns dos pacotes npm de que você depende já estejam usando recursos de linguagem modernos. Há várias opções disponíveis para usar o código moderno do npm sem corromper o aplicativo em navegadores mais antigos, mas a ideia geral é fazer com que o sistema de compilação transpile as dependências para o mesmo destino de sintaxe do código-fonte.

Webpack

A partir do webpack 5, agora é possível configurar qual sintaxe o webpack vai usar ao gerar código para pacotes e módulos. Isso não transcompila o código ou as dependências, apenas afeta o código "agrupador" gerado pelo webpack. Para especificar o suporte a navegadores, adicione uma configuração de browserslist ao projeto ou faça isso diretamente na configuração do webpack:

module.exports = {
  target: ['web', 'es2017'],
};

Também é possível configurar o webpack para gerar pacotes otimizados que omitem funções de wrapper desnecessárias ao segmentar um ambiente moderno de módulos ES. Isso também configura o webpack para carregar pacotes de divisão de código usando <script type="module">.

module.exports = {
  target: ['web', 'es2017'],
  output: {
    module: true,
  },
  experiments: {
    outputModule: true,
  },
};

Há vários plug-ins de webpack disponíveis que possibilitam a compilação e envio de JavaScript moderno sem deixar de oferecer suporte a navegadores legados, como o plug-in do Optimize e o BabelEsmPlugin.

Plug-in do Optimize

O plug-in do Optimize é um plug-in webpack que transforma o código empacotado final de JavaScript moderno para legado, em vez de cada arquivo de origem individual. Trata-se de uma configuração independente que permite que a configuração do webpack presuma que tudo seja um JavaScript moderno, sem ramificações especiais para várias saídas ou sintaxes.

Como o plug-in do Optimize opera em pacotes em vez de módulos individuais, ele processa o código do aplicativo e as dependências igualmente. Isso torna seguro usar dependências JavaScript modernas do npm, porque o código será agrupado e transcompilado para a sintaxe correta. Ela também pode ser mais rápida do que as soluções tradicionais que envolvem duas etapas de compilação e, ao mesmo tempo, gerar pacotes separados para navegadores modernos e legados. Os dois conjuntos de pacotes foram projetados para serem carregados usando o padrão módulo/nomodule (link em inglês).

// webpack.config.js
const OptimizePlugin = require('optimize-plugin');

module.exports = {
  // ...
  plugins: [new OptimizePlugin()],
};

O Optimize Plugin pode ser mais rápido e eficiente do que as configurações personalizadas do webpack, que normalmente agrupam o código moderno e legado separadamente. Ele também processa a execução do Babel e reduz os pacotes usando o Terser com configurações ideais separadas para as saídas modernas e legadas. Por fim, os polyfills necessários para os pacotes legados gerados são extraídos em um script dedicado para que nunca sejam duplicados ou carregados desnecessariamente em navegadores mais recentes.

Comparação: transcompilar módulos de origem duas vezes versus transcompilar pacotes gerados.

BabelEsmPlugin

O BabelEsmPlugin é um plug-in webpack que funciona com @babel/preset-env (em inglês) para gerar versões modernas de pacotes existentes e enviar códigos menos transcompilados para navegadores modernos. É a solução mais conhecida para módulo/nomódulo, usada pela Next.js e CLI Preact.

// webpack.config.js
const BabelEsmPlugin = require('babel-esm-plugin');

module.exports = {
  //...
  module: {
    rules: [
      // your existing babel-loader configuration:
      {
        test: /\.js$/,
        exclude: /node_modules/,
        use: {
          loader: 'babel-loader',
          options: {
            presets: ['@babel/preset-env'],
          },
        },
      },
    ],
  },
  plugins: [new BabelEsmPlugin()],
};

BabelEsmPlugin oferece suporte a uma ampla variedade de configurações de webpack, porque executa duas versões bastante separadas do aplicativo. A compilação duas vezes pode levar um pouco mais de tempo para aplicativos grandes. No entanto, essa técnica permite que o BabelEsmPlugin se integre perfeitamente às configurações atuais do webpack, o que a torna uma das opções mais convenientes disponíveis.

Configurar o babel-loader para transcompilar node_modules

Se você estiver usando babel-loader sem um dos dois plug-ins anteriores, será necessário realizar uma etapa importante para consumir módulos npm modernos do JavaScript. A definição de duas configurações babel-loader separadas possibilita compilar automaticamente recursos de linguagem modernos encontrados em node_modules para ES2017, enquanto ainda transcompila seu próprio código próprio com os plug-ins e as predefinições do Babel definidos na configuração do projeto. Isso não gera pacotes modernos e legados para uma configuração de módulo/nomódulo, mas permite instalar e usar pacotes npm que contêm JavaScript moderno sem corromper navegadores mais antigos.

O webpack-plugin-modern-npm usa essa técnica para compilar dependências npm que têm um campo "exports" no package.json, já que elas podem conter sintaxe moderna:

// webpack.config.js
const ModernNpmPlugin = require('webpack-plugin-modern-npm');

module.exports = {
  plugins: [
    // auto-transpile modern stuff found in node_modules
    new ModernNpmPlugin(),
  ],
};

Como alternativa, é possível implementar a técnica manualmente na configuração do webpack, verificando se há um campo "exports" no package.json dos módulos à medida que eles são resolvidos. Para simplificar o armazenamento em cache, uma implementação personalizada terá esta aparência:

// webpack.config.js
module.exports = {
  module: {
    rules: [
      // Transpile for your own first-party code:
      {
        test: /\.js$/i,
        loader: 'babel-loader',
        exclude: /node_modules/,
      },
      // Transpile modern dependencies:
      {
        test: /\.js$/i,
        include(file) {
          let dir = file.match(/^.*[/\\]node_modules[/\\](@.*?[/\\])?.*?[/\\]/);
          try {
            return dir && !!require(dir[0] + 'package.json').exports;
          } catch (e) {}
        },
        use: {
          loader: 'babel-loader',
          options: {
            babelrc: false,
            configFile: false,
            presets: ['@babel/preset-env'],
          },
        },
      },
    ],
  },
};

Ao usar essa abordagem, você precisará garantir que a sintaxe moderna tenha suporte no seu minificador. Terser e uglify-es têm uma opção para especificar {ecma: 2017} a fim de preservar e, em alguns casos, gerar sintaxe ES2017 durante a compactação e a formatação.

Consolidação

O Rollup tem suporte integrado para gerar vários conjuntos de pacotes como parte de um único build e gera um código moderno por padrão. Como resultado, o Rollup pode ser configurado para gerar pacotes modernos e legados com os plug-ins oficiais que você provavelmente já usa.

@rollup/plugin-babel

Se você usar o Rollup, o método getBabelOutputPlugin() (fornecido pelo plug-in oficial do Babel do Rollup) transformará o código em pacotes gerados em vez de módulos de origem individuais. O Rollup tem suporte integrado para gerar vários conjuntos de pacotes como parte de um único build, cada um com os próprios plug-ins. Ele pode ser usado para produzir diferentes pacotes para modernos e legados, passando cada um por uma configuração diferente de plug-in de saída do Babel:

// rollup.config.js
import {getBabelOutputPlugin} from '@rollup/plugin-babel';

export default {
  input: 'src/index.js',
  output: [
    // modern bundles:
    {
      format: 'es',
      plugins: [
        getBabelOutputPlugin({
          presets: [
            [
              '@babel/preset-env',
              {
                targets: {esmodules: true},
                bugfixes: true,
                loose: true,
              },
            ],
          ],
        }),
      ],
    },
    // legacy (ES5) bundles:
    {
      format: 'amd',
      entryFileNames: '[name].legacy.js',
      chunkFileNames: '[name]-[hash].legacy.js',
      plugins: [
        getBabelOutputPlugin({
          presets: ['@babel/preset-env'],
        }),
      ],
    },
  ],
};

Outras ferramentas de build

O Rollup e o Webpack são altamente configuráveis, o que geralmente significa que cada projeto precisa atualizar a própria configuração para ativar a sintaxe JavaScript moderna nas dependências. Há também ferramentas de build de nível superior que favorecem a convenção e os padrões em vez da configuração, como Parcel, Snowpack, Vite e WMR. A maioria dessas ferramentas supõe que as dependências npm podem conter sintaxe moderna e as transcompilará para os níveis de sintaxe adequados ao criar para produção.

Além de plug-ins dedicados para webpack e Rollup, os pacotes modernos de JavaScript com substitutos legados podem ser adicionados a qualquer projeto usando o devolution. O Devolution é uma ferramenta autônoma que transforma a saída de um sistema de build para produzir variantes de JavaScript legadas, permitindo que agrupamentos e transformações assumam um destino de saída moderno.