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

Melhore a performance ativando as dependências e saídas modernas do JavaScript.

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

JavaScript moderno

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

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

Os recursos nas versões mais recentes da especificação de linguagem geralmente têm suporte menos consistente nos navegadores modernos. Por exemplo, muitos recursos do ES2020 e ES2021 têm suporte apenas em 70% do mercado de navegadores, ou seja, ainda são 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 alvo em movimento, o ES2017 tem a maior variedade de compatibilidade de navegador e inclui a maioria dos recursos de sintaxe modernos mais usados. Em outras palavras, o ES2017 é o mais próximo da sintaxe moderna.

JavaScript legado

O 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 a sintaxe moderna, mas compila tudo para a sintaxe legada para aumentar o suporte ao navegador. A compilação para a sintaxe legada aumenta o suporte ao navegador, mas o efeito é geralmente menor do que imaginamos. Em muitos casos, o suporte aumenta de cerca de 95% para 98%, gerando um custo significativo:

  • O JavaScript legado geralmente é cerca de 20% maior e mais lento do que o código moderno equivalente. As deficiências de ferramentas e a configuração incorreta geralmente ampliam ainda mais essa lacuna.

  • As bibliotecas instaladas representam até 90% do código JavaScript de produção típico. O código da biblioteca tem um overhead de JavaScript legado ainda maior devido à duplicação de polyfill e auxiliar que poderia ser evitada com a publicação de um código moderno.

JavaScript moderno no npm

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

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

Os módulos referenciados pelo campo "exports" implicam uma versão do Node de pelo menos 12.8, que oferece suporte ao ES2019. Isso significa que qualquer módulo referenciado usando o campo "exports" pode ser gravado em JavaScript moderno. Os consumidores de pacotes precisam supor que os módulos com um campo "exports" contêm código moderno e transpilam, se necessário.

Somente moderno

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

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

Moderno com fallback legado

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

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

Moderno com otimizações de fallback legada e bundler de ESM

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

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

Muitos bundlers, como o webpack e o 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. Use essa abordagem para enviar código moderno com um fallback legado que ainda está otimizado para agrupamento.

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 legada do ES5, isso não é mais uma suposição segura e pode fazer com que as atualizações de dependências interrompam o suporte do navegador no seu 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 processá-los. Há uma boa chance de que alguns dos pacotes do 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 interromper o aplicativo em navegadores mais antigos, mas a ideia geral é fazer com que o sistema de build 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 transpila seu código ou dependências, apenas afeta o código "glue" gerado pelo webpack. Para especificar o destino de suporte ao navegador, 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 omitirão 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 do webpack disponíveis que permitem compilar e enviar JavaScript moderno, mantendo o suporte a navegadores legados, como o Optimize Plugin e o BabelEsmPlugin.

Plug-in do Optimize

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

Como o plug-in Optimize opera em pacotes em vez de módulos individuais, ele processa o código do aplicativo e as dependências da mesma forma. Isso torna seguro usar dependências JavaScript modernas do npm, porque o código delas será empacotado e transpilado para a sintaxe correta. Ele também pode ser mais rápido do que soluções tradicionais que envolvem duas etapas de compilação, gerando pacotes separados para navegadores modernos e legados. Os dois conjuntos de pacotes são projetados para serem carregados usando o padrão módulo/sem módulo.

// 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 geralmente agrupam o código moderno e legado separadamente. Ele também processa a execução do Babel para você e minimiza pacotes usando o Terser com configurações ideais separadas para as saídas modernas e legados. Por fim, os polyfills necessários pelos 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: transpilação de módulos de origem duas vezes em vez de transpilar pacotes gerados.

BabelEsmPlugin

O BabelEsmPlugin é um plug-in do webpack que funciona com o @babel/preset-env para gerar versões modernas de pacotes existentes e enviar menos código transpilado para navegadores modernos. É a solução pronta mais conhecida para module/nomodule, usada pelo Next.js e pela CLI do 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()],
};

O BabelEsmPlugin oferece suporte a uma ampla variedade de configurações do webpack, porque executa dois builds amplamente separados do aplicativo. A compilação duas vezes pode levar um pouco mais de tempo para aplicativos grandes. No entanto, essa técnica permite que BabelEsmPlugin seja integrado perfeitamente às configurações atuais do webpack e se torne uma das opções mais convenientes disponíveis.

Configurar o babel-loader para transpilar node_modules

Se você estiver usando babel-loader sem um dos dois plug-ins anteriores, uma etapa importante será necessária para consumir módulos npm JavaScript modernos. A definição de duas configurações babel-loader separadas permite compilar automaticamente os recursos de linguagem modernos encontrados em node_modules para ES2017, enquanto ainda transpila seu próprio código proprietário com os plug-ins e 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/sem módulo, mas permite instalar e usar pacotes npm que contêm JavaScript moderno sem interromper navegadores mais antigos.

O webpack-plugin-modern-npm usa essa técnica para compilar dependências do 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 um campo "exports" no package.json dos módulos conforme eles são resolvidos. Omitindo o armazenamento em cache para encurtar, uma implementação personalizada pode 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ê precisa garantir que a sintaxe moderna seja compatível com seu minificador. Tanto o Terser quanto o uglify-es têm uma opção para especificar {ecma: 2017} a fim de preservar e, em alguns casos, gerar a sintaxe ES2017 durante a compactação e formatação.

Consolidação

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

@rollup/plugin-babel

Se você usar o Rollup, o método getBabelOutputPlugin() (fornecido pelo plug-in oficial do Babel) transforma o código em pacotes gerados, em vez de módulos de origem individuais. O agrupamento tem suporte integrado para gerar vários conjuntos de pacotes como parte de um único build, cada um com seus próprios plug-ins. Você pode usar isso para produzir pacotes diferentes para modernos e legados transmitindo cada um por uma configuração diferente do 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 configuração para ativar a sintaxe JavaScript moderna nas dependências. Também há ferramentas de build de nível mais alto que favorecem a convenção e os padrões em vez da configuração, como Parcel, Snowpack, Vite e WMR. A maioria dessas ferramentas assume que as dependências do npm podem conter sintaxe moderna e as transpila para os níveis de sintaxe adequados ao criar para produção.

Além de plug-ins dedicados para webpack e Rollup, pacotes modernos de JavaScript com substitutos legados podem ser adicionados a qualquer projeto usando devolution. A devolução é uma ferramenta independente que transforma a saída de um sistema de build para produzir variantes legado do JavaScript, permitindo que o agrupamento e as transformações assumam um destino de saída moderno.