Publica, instala y envía JavaScript moderno para aplicaciones más rápidas

Activa las dependencias y la salida de JavaScript modernas para mejorar el rendimiento.

Más del 90% de los navegadores pueden ejecutar JavaScript moderno, pero la prevalencia de JavaScript heredado sigue siendo una gran fuente de problemas de rendimiento en la Web en la actualidad.

JavaScript moderno

El JavaScript moderno no se caracteriza como código escrito en una versión específica de la especificación de ECMAScript, sino en una sintaxis que es compatible con todos los navegadores modernos. Los navegadores web modernos, como Chrome, Edge, Firefox y Safari, representan más del 90% del mercado de navegadores, y los diferentes navegadores que dependen de los mismos motores de renderización subyacentes representan un 5% adicional. Esto significa que el 95% del tráfico web global proviene de navegadores que admiten las funciones del lenguaje JavaScript más utilizadas en los últimos 10 años, incluidas las siguientes:

  • Clases (ES2015)
  • Funciones de flecha (ES2015)
  • Generadores (ES2015)
  • Alcance de bloque (ES2015)
  • Desestructuración (ES2015)
  • Parámetros rest y spread (ES2015)
  • Abreviatura de objetos (ES2015)
  • Async/await (ES2017)

Por lo general, las funciones de las versiones más recientes de la especificación del lenguaje tienen una compatibilidad menos coherente en los navegadores modernos. Por ejemplo, muchas funciones de ES2020 y ES2021 solo son compatibles con el 70% del mercado de navegadores, lo que sigue siendo la mayoría de los navegadores, pero no es suficiente para que sea seguro depender de esas funciones directamente. Esto significa que, aunque JavaScript "moderno" es un objetivo en movimiento, ES2017 tiene la gama más amplia de compatibilidad con navegadores y, al mismo tiempo, incluye la mayoría de las funciones de sintaxis modernas de uso general. En otras palabras, ES2017 es lo más cercano a la sintaxis moderna en la actualidad.

JavaScript heredado

El código JavaScript heredado es un código que evita específicamente el uso de todas las funciones del lenguaje anteriores. La mayoría de los desarrolladores escriben su código fuente con sintaxis moderna, pero compilan todo en sintaxis heredada para aumentar la compatibilidad con los navegadores. La compilación para la sintaxis heredada aumenta la compatibilidad con los navegadores, pero el efecto suele ser menor de lo que creemos. En muchos casos, la disponibilidad aumenta de alrededor del 95% al 98% y, al mismo tiempo, se incurre en un costo significativo:

  • Por lo general, el código JavaScript heredado es alrededor de un 20% más grande y lento que el código moderno equivalente. Las deficiencias en las herramientas y la configuración incorrecta a menudo widen esta brecha aún más.

  • Las bibliotecas instaladas representan hasta el 90% del código de producción típico de JavaScript. El código de la biblioteca incurrirá en una sobrecarga de JavaScript heredada aún mayor debido a la duplicación de polyfill y ayudantes que se podría evitar si se publicara código moderno.

JavaScript moderno en npm

Recientemente, Node.js estandarizó un campo "exports" para definir los puntos de entrada de un paquete:

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

Los módulos a los que hace referencia el campo "exports" implican una versión de Node de al menos 12.8, que admite ES2019. Esto significa que cualquier módulo al que se haga referencia con el campo "exports" se puede escribir en JavaScript moderno. Los consumidores de paquetes deben asumir que los módulos con un campo "exports" contienen código moderno y transpilarlos si es necesario.

Solo moderno

Si quieres publicar un paquete con código moderno y dejar que el consumidor se encargue de transpilarlo cuando lo use como dependencia, usa solo el campo "exports".

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

Moderno con resguardo heredado

Usa el campo "exports" junto con "main" para publicar tu paquete con código moderno, pero también incluye un resguardo de ES5 + CommonJS para navegadores heredados.

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

Moderno con resguardo heredado y optimizaciones del empaquetador de ESM

Además de definir un punto de entrada de CommonJS de resguardo, el campo "module" se puede usar para apuntar a un paquete de resguardo heredado similar, pero que use la sintaxis del módulo de JavaScript (import y export).

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

Muchos empaquetadores, como webpack y Rollup, dependen de este campo para aprovechar las funciones del módulo y habilitar el sacudido de árboles. Este sigue siendo un paquete heredado que no contiene ningún código moderno, además de la sintaxis import/export, por lo que debes usar este enfoque para enviar código moderno con un resguardo heredado que aún esté optimizado para el empaquetado.

JavaScript moderno en aplicaciones

Las dependencias de terceros conforman la gran mayoría del código JavaScript de producción típico en las aplicaciones web. Si bien, históricamente, las dependencias de npm se publicaron como sintaxis ES5 heredada, esta ya no es una suposición segura y corre el riesgo de que las actualizaciones de dependencias rompan la compatibilidad del navegador en tu aplicación.

Con una cantidad cada vez mayor de paquetes de npm que se trasladan a JavaScript moderno, es importante asegurarse de que las herramientas de compilación estén configuradas para controlarlos. Es probable que algunos de los paquetes de npm de los que dependes ya usen funciones de lenguaje modernas. Hay varias opciones disponibles para usar código moderno de npm sin interrumpir tu aplicación en navegadores más antiguos, pero la idea general es que el sistema de compilación transpile las dependencias al mismo objetivo de sintaxis que tu código fuente.

webpack

A partir de webpack 5, ahora es posible configurar qué sintaxis usará webpack cuando genere código para paquetes y módulos. Esto no transpila tu código ni tus dependencias, sino que solo afecta el código "adhesivo" que genera webpack. Para especificar el objetivo de compatibilidad con el navegador, agrega una configuración de browserslist a tu proyecto o hazlo directamente en la configuración de webpack:

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

También es posible configurar webpack para generar paquetes optimizados que omitan las funciones de wrapper innecesarias cuando se segmenta un entorno moderno de módulos ES. Esto también configura webpack para que cargue paquetes divididos por código con <script type="module">.

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

Existen varios complementos de webpack disponibles que permiten compilar y enviar JavaScript moderno sin dejar de admitir navegadores heredados, como Optimize Plugin y BabelEsmPlugin.

Complemento de Optimize

El complemento Optimize es un complemento de Webpack que transforma el código empaquetado final de JavaScript moderno a heredado en lugar de cada archivo de origen individual. Es una configuración independiente que permite que tu configuración de webpack asuma que todo es JavaScript moderno sin ramificaciones especiales para varias salidas o sintaxis.

Dado que el complemento Optimize funciona en paquetes en lugar de módulos individuales, procesa el código de tu aplicación y tus dependencias por igual. Esto hace que sea seguro usar dependencias de JavaScript modernas desde npm, ya que su código se agrupará y transpilará a la sintaxis correcta. También puede ser más rápido que las soluciones tradicionales que involucran dos pasos de compilación, a la vez que genera paquetes separados para navegadores modernos y heredados. Los dos conjuntos de paquetes están diseñados para cargarse con el patrón module/nomodule.

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

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

Optimize Plugin puede ser más rápido y eficiente que las configuraciones personalizadas de webpack, que suelen agrupar el código moderno y heredado por separado. También se encarga de ejecutar Babel por ti y reduce los paquetes con Terser con parámetros de configuración óptimos independientes para los resultados modernos y heredados. Por último, los polyfills que necesitan los paquetes heredados generados se extraen en una secuencia de comandos dedicada para que nunca se dupliquen ni se carguen innecesariamente en navegadores más nuevos.

Comparación: transpilación de módulos de origen dos veces en comparación con la transpilación de paquetes generados.

BabelEsmPlugin

BabelEsmPlugin es un complemento de Webpack que funciona junto con @babel/preset-env para generar versiones modernas de los paquetes existentes y enviar menos código transpilado a los navegadores modernos. Es la solución lista para usar más popular para module/nomodule, que usan Next.js y Preact CLI.

// 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 admite una amplia variedad de configuraciones de webpack, ya que ejecuta dos compilaciones de tu aplicación bastante separadas. La compilación dos veces puede llevar un poco más de tiempo para aplicaciones grandes. Sin embargo, esta técnica permite que BabelEsmPlugin se integre sin problemas en las configuraciones de Webpack existentes y la convierte en una de las opciones más convenientes disponibles.

Configura babel-loader para transpilar node_modules

Si usas babel-loader sin uno de los dos complementos anteriores, hay un paso importante que se requiere para consumir módulos npm de JavaScript modernos. Definir dos configuraciones de babel-loader separadas permite compilar automáticamente las funciones de lenguaje modernas que se encuentran en node_modules a ES2017, mientras se transpila tu propio código propio con los complementos y ajustes predeterminados de Babel definidos en la configuración de tu proyecto. Esto no genera paquetes modernos y heredados para una configuración de módulo/no módulo, pero permite instalar y usar paquetes de npm que contienen JavaScript moderno sin dañar los navegadores más antiguos.

webpack-plugin-modern-npm usa esta técnica para compilar dependencias de npm que tienen un campo "exports" en su package.json, ya que pueden contener sintaxis 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, puedes implementar la técnica de forma manual en la configuración de webpack. Para ello, busca un campo "exports" en el package.json de los módulos a medida que se resuelven. Si omitimos el almacenamiento en caché para ahorrar espacio, una implementación personalizada podría verse de la siguiente manera:

// 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'],
          },
        },
      },
    ],
  },
};

Cuando uses este enfoque, deberás asegurarte de que tu minificador admita la sintaxis moderna. Tanto Terser como uglify-es tienen la opción de especificar {ecma: 2017} para preservar y, en algunos casos, generar sintaxis ES2017 durante la compresión y el formato.

Resumen

Rollup tiene compatibilidad integrada para generar varios conjuntos de paquetes como parte de una sola compilación y genera código moderno de forma predeterminada. Como resultado, Rollup se puede configurar para generar paquetes modernos y heredados con los complementos oficiales que probablemente ya estés usando.

@rollup/plugin-babel

Si usas Rollup, el método getBabelOutputPlugin() (proporcionado por el complemento oficial de Babel de Rollup) transforma el código en paquetes generados en lugar de módulos de origen individuales. Rollup tiene compatibilidad integrada para generar varios conjuntos de paquetes como parte de una sola compilación, cada uno con sus propios complementos. Puedes usar esto para producir distintos paquetes para versiones modernas y heredadas pasando cada uno a través de una configuración de plugin de salida de Babel diferente:

// 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'],
        }),
      ],
    },
  ],
};

Herramientas de compilación adicionales

Rollup y Webpack son altamente configurables, lo que, en general, significa que cada proyecto debe actualizar su configuración para habilitar la sintaxis moderna de JavaScript en las dependencias. También hay herramientas de compilación de nivel superior que favorecen las convenciones y los valores predeterminados sobre la configuración, como Parcel, Snowpack, Vite y WMR. La mayoría de estas herramientas asumen que las dependencias de npm pueden contener sintaxis moderna y las transpilarán a los niveles de sintaxis adecuados cuando se compilen para producción.

Además de los complementos dedicados para webpack y Rollup, se pueden agregar paquetes modernos de JavaScript con resguardos heredados a cualquier proyecto con devolution. Devolution es una herramienta independiente que transforma el resultado de un sistema de compilación para producir variantes de JavaScript heredadas, lo que permite que el empaquetado y las transformaciones asuman un objetivo de salida moderno.