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

Activa las dependencias y los resultados modernos de JavaScript 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 por escribir código en una versión específica de la especificación ECMAScript, sino en una sintaxis que todos los navegadores modernos admiten. Los navegadores web modernos, como Chrome, Edge, Firefox y Safari, conforman más del 90% del mercado de navegadores, y los diferentes navegadores que dependen de los mismos motores de renderización subyacentes constituyen 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 usadas de los últimos 10 años, entre las que se incluyen las siguientes:

  • Clases (ES2015)
  • Funciones de flecha (ES2015)
  • Generadores (ES2015)
  • Alcance del bloque (ES2015)
  • Desestructuración (ES2015)
  • Parámetros de reposo y expansión (ES2015)
  • Abreviatura del objeto (ES2015)
  • Async/await (ES2017)

Por lo general, las funciones de las versiones más recientes de la especificación de lenguaje tienen una compatibilidad menos coherente en los navegadores modernos. Por ejemplo, muchas funciones de ES2020 y ES2021 solo se admiten en el 70% del mercado de navegadores; todavía es la mayoría de los navegadores, pero no lo suficiente como para que sea seguro confiar directamente en esas funciones. Esto significa que, aunque JavaScript "moderno" es un destino móvil, ES2017 tiene el rango más amplio de compatibilidad con navegadores, si bien incluye la mayoría de las funciones de sintaxis modernas de uso general. En otras palabras, ES2017 es la sintaxis más cercana a la sintaxis moderna en la actualidad.

JavaScript heredado

El JavaScript heredado es 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 una sintaxis moderna, pero compilan todo en la sintaxis heredada para una mayor compatibilidad con los navegadores. Compilar en una sintaxis heredada aumenta la compatibilidad con el navegador; sin embargo, el efecto suele ser menor de lo que creemos. En muchos casos, la compatibilidad aumenta de alrededor del 95% al 98% y, a su vez, se genera un costo significativo:

  • El JavaScript heredado suele ser un 20% más grande y más lento que el código moderno equivalente. Las deficiencias de las herramientas y la configuración incorrecta a menudo amplían aún más esta brecha.

  • Las bibliotecas instaladas representan casi el 90% del código JavaScript de producción típico. El código de biblioteca genera una sobrecarga de JavaScript heredada mayor debido a la duplicación de polyfills y asistentes que se podrían evitar publicando código moderno.

JavaScript moderno en npm

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

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

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

Solo moderno

Si deseas publicar un paquete con código moderno y dejar que el consumidor se encargue de la transpilación 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 agrupador de ESM

Además de definir un punto de entrada de resguardo de CommonJS, 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 agrupadores, como webpack y Rollup, dependen de este campo para aprovechar las funciones del módulo y habilitar la eliminación de código no utilizado. Sigue siendo un paquete heredado que no contiene ningún código moderno aparte de la sintaxis import o export, por lo que debes usar este enfoque para enviar código moderno con un resguardo heredado que aún esté optimizado para los paquetes.

JavaScript moderno en aplicaciones

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

Debido a que la cantidad cada vez mayor de paquetes de npm se trasladan al JavaScript moderno, es importante asegurarte de que las herramientas de compilación estén configuradas para controlarlos. Es muy probable que algunos de los paquetes de npm de los que dependes ya usen funciones modernas de lenguaje. Hay varias opciones disponibles para usar un código moderno de npm sin dañar tu aplicación en navegadores más antiguos, pero la idea general es hacer que el sistema de compilación transpila dependencias a la misma orientación de sintaxis que tu código fuente.

Webpack

A partir de webpack 5, ahora es posible configurar la sintaxis que usará Webpack cuando genere código para paquetes y módulos. Esta acción no transpila el código ni las dependencias, solo afecta el código de "unión" que genera webpack. Para especificar el objetivo de compatibilidad del navegador, agrega una configuración de lista de navegadores a tu proyecto o hazlo directamente en la configuración de tu paquete web:

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

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

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

Hay una serie de complementos de webpack disponibles que permiten compilar y enviar JavaScript moderno y, al mismo tiempo, admitir navegadores heredados, como el complemento Optimize y BabelEsmPlugin.

Complemento Optimize

El complemento Optimize es un complemento de webpack que transforma el paquete final de código de JavaScript moderno a uno heredado, en lugar de cada archivo fuente individual. Es una configuración independiente que permite que la configuración de webpack asuma que todo es JavaScript moderno, sin ramas especiales para varias salidas o sintaxis.

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

// 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 webpacks, que suelen agrupar el código moderno y heredado por separado. También controla la ejecución de Babel y reduce los paquetes mediante Terser con opciones de configuración óptimas independientes para las salidas modernas y heredadas. 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: transpilar módulos de origen dos veces en comparación con transpilar paquetes generados.

BabelEsmPlugin

BabelEsmPlugin es un complemento de webpack que funciona con @babel/preset-env para generar versiones modernas de paquetes existentes a fin de enviar menos código transpilado a navegadores modernos. Es la solución lista para usar más popular para module/nomodule y la usan Next.js y la CLI de 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 admite una amplia variedad de configuraciones de webpack, ya que ejecuta dos compilaciones independientes de tu aplicación. Compilar dos veces puede llevar un poco de tiempo adicional para las aplicaciones grandes. Sin embargo, esta técnica permite que BabelEsmPlugin se integre sin problemas en las configuraciones existentes de webpack y hace que sea una de las opciones más convenientes disponibles.

Cómo configurar Babel-loader para transpilar node_modules

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

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 una 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 si buscas un campo "exports" en el package.json de los módulos a medida que se resuelven. Si se omite el almacenamiento en caché por cuestiones de brevedad, 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 la sintaxis ES2017 durante la compresión y el formateo.

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 fuente 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 diferentes paquetes para versiones modernas y heredadas. Para ello, pasa cada uno a través de una configuración diferente del complemento de salida de 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'],
        }),
      ],
    },
  ],
};

Herramientas de compilación adicionales

La opción Rollup y webpack son altamente configurables, lo que, por lo general, significa que cada proyecto debe actualizar su configuración para habilitar la sintaxis moderna de JavaScript en las dependencias. También existen herramientas de compilación de nivel superior que favorecen las convenciones y los valores predeterminados por sobre la configuración, como Parcel, Snowpack, Vite y WMR. La mayoría de estas herramientas suponen que las dependencias de npm pueden contener una sintaxis moderna y las transpilarán a los niveles de sintaxis adecuados cuando realices compilaciones 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 mediante la devolución. Devolution es una herramienta independiente que transforma el resultado de un sistema de compilación para producir variantes heredadas de JavaScript, lo que permite agrupar y realizar transformaciones para asumir un objetivo de salida moderno.