Publica código moderno en navegadores actualizados para que las páginas se carguen más rápido

En este codelab, mejorarás el rendimiento de esta aplicación sencilla que permite a los usuarios calificar gatos aleatorios. Aprende a optimizar el paquete de JavaScript minimizando la cantidad de código que se transpila.

Captura de pantalla de la aplicación

En la app de ejemplo, puedes seleccionar una palabra o un emoji para indicar cuánto te gusta cada gato. Cuando haces clic en un botón, la app muestra el valor del botón debajo de la imagen del gato actual.

Medir

Siempre es una buena idea comenzar por inspeccionar un sitio web antes de agregar optimizaciones:

  1. Para obtener una vista previa del sitio, presiona View App y, luego, Fullscreen pantalla completa.
  2. Presiona "Control + Mayúsculas + J" (o "Comando + Opción + J" en Mac) para abrir las Herramientas para desarrolladores.
  3. Haga clic en la pestaña Red.
  4. Selecciona la casilla de verificación Inhabilitar la caché.
  5. Vuelve a cargar la app.

Solicitud de tamaño del paquete original

Se usan más de 80 KB para esta aplicación. Es hora de averiguar si hay partes del paquete que no se están usando:

  1. Presiona Control+Shift+P (o Command+Shift+P en Mac) para abrir el menú Comando. Menú de comandos

  2. Ingresa Show Coverage y presiona Enter para mostrar la pestaña Cobertura.

  3. En la pestaña Coverage, haz clic en Reload para volver a cargar la aplicación mientras se captura la cobertura.

    Vuelve a cargar la app con la cobertura de código

  4. Observa cuánto código se usó en comparación con cuánto se cargó para el paquete principal:

    Cobertura de código del paquete

Más de la mitad del paquete (44 KB) ni siquiera se utiliza. Esto se debe a que gran parte del código que contiene consiste en polyfills para garantizar que la aplicación funcione en navegadores más antiguos.

Usa @babel/preset-env

La sintaxis del lenguaje JavaScript se ajusta a un estándar conocido como ECMAScript o ECMA-262. Cada año, se lanzan versiones más recientes de la especificación, que incluyen funciones nuevas que aprobaron el proceso de propuesta. Cada navegador importante siempre se encuentra en una etapa diferente de compatibilidad con estas funciones.

En la aplicación, se usan las siguientes funciones de ES2015:

También se usa la siguiente función de ES2017:

No dudes en consultar el código fuente en src/index.js para ver cómo se usa todo esto.

Todas estas funciones son compatibles con la versión más reciente de Chrome, pero ¿qué sucede con otros navegadores que no las admiten? Babel, que se incluye en la aplicación, es la biblioteca más popular que se usa para compilar código que contiene una sintaxis más nueva en código que los navegadores y entornos más antiguos pueden comprender. Para ello, hace lo siguiente:

  • Se incluyen polyfills para emular las funciones más recientes de ES2015+, de modo que sus APIs se puedan usar incluso si el navegador no las admite. Este es un ejemplo de un polyfill del método Array.includes.
  • Los complementos se usan para transformar el código de ES2015 (o posterior) en la sintaxis de ES5 anterior. Como se trata de cambios relacionados con la sintaxis (como las funciones de flecha), no se pueden emular con polyfills.

Consulta package.json para ver qué bibliotecas de Babel se incluyen:

"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 es el compilador principal de Babel. Con esto, todos los parámetros de configuración de Babel se definen en un .babelrc en la raíz del proyecto.
  • babel-loader incluye Babel en el proceso de compilación de webpack.

Ahora, observa webpack.config.js para ver cómo se incluye babel-loader como una regla:

module: {
  rules: [
    //...
    {
      test: /\.js$/,
      exclude: /node_modules/,
      loader: "babel-loader"
    }
  ]
},
  • @babel/polyfill proporciona todos los polyfills necesarios para las funciones de ECMAScript más recientes, de modo que puedan funcionar en entornos que no las admiten. Ya se importó en la parte superior de src/index.js.
import "./style.css";
import "@babel/polyfill";
  • @babel/preset-env identifica qué transformaciones y polyfills son necesarios para los navegadores o entornos elegidos como destino.

Consulta el archivo de configuración de Babel, .babelrc, para ver cómo se incluye:

{
  "presets": [
    [
      "@babel/preset-env",
      {
        "targets": "last 2 versions"
      }
    ]
  ]
}

Esta es una configuración de Babel y webpack. Obtén información para incluir Babel en tu aplicación si usas un integrador de módulos diferente de webpack.

El atributo targets en .babelrc identifica los navegadores objetivo. @babel/preset-env se integra con browserslist, lo que significa que puedes encontrar una lista completa de consultas compatibles que se pueden usar en este campo en la documentación de browserlist.

El valor "last 2 versions" transpila el código de la aplicación para las dos versiones más recientes de cada navegador.

Depuración

Para obtener una vista completa de todos los destinos de Babel del navegador, así como de todas las transformaciones y polyfills que se incluyen, agrega un campo debug a .babelrc:

{
  "presets": [
    [
      "@babel/preset-env",
      {
        "targets": "last 2 versions",
        "debug": true
      }
    ]
  ]
}
  • Haga clic en Herramientas.
  • Haz clic en Registros.

Vuelve a cargar la aplicación y observa los registros de estado de Glitch en la parte inferior del editor.

Navegadores de destino

Babel registra varios detalles en la consola sobre el proceso de compilación, incluidos todos los entornos de destino para los que se compiló el código.

Navegadores de destino

Observa cómo se incluyen en esta lista los navegadores descontinuados, como Internet Explorer. Esto es un problema porque los navegadores no compatibles no tendrán funciones más nuevas agregadas, y Babel seguirá transpilando la sintaxis específica para ellos. Esto aumenta innecesariamente el tamaño de tu paquete si los usuarios no usan este navegador para acceder a tu sitio.

Babel también registra una lista de los complementos de transformación que se usaron:

Lista de complementos usados

¡Es una lista bastante larga! Estos son todos los complementos que Babel necesita usar para transformar cualquier sintaxis de ES2015+ en una sintaxis más antigua para todos los navegadores de destino.

Sin embargo, Babel no muestra ningún polyfill específico que se use:

No se agregaron polyfills

Esto se debe a que se importa directamente todo el @babel/polyfill.

Carga polyfills de forma individual

De forma predeterminada, Babel incluye todos los polyfills necesarios para un entorno ES2015+ completo cuando se importa @babel/polyfill a un archivo. Para importar polyfills específicos necesarios para los navegadores de destino, agrega un useBuiltIns: 'entry' a la configuración.

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

Vuelve a cargar la aplicación. Ahora puedes ver todos los polyfills específicos incluidos:

Lista de polyfills importados

Aunque ahora solo se incluyen los polyfills necesarios para "last 2 versions", la lista sigue siendo muy larga. Esto se debe a que se siguen incluyendo los polyfills necesarios para los navegadores de destino de cada función más reciente. Cambia el valor del atributo a usage para incluir solo los que se necesitan para las funciones que se usan en el código.

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

Con esto, los polyfills se incluyen automáticamente donde se necesitan. Esto significa que puedes quitar la importación de @babel/polyfill en src/index.js..

import "./style.css";
import "@babel/polyfill";

Ahora, solo se incluyen los polyfills necesarios para la aplicación.

Lista de polyfills incluidos automáticamente

El tamaño del paquete de la aplicación se reduce significativamente.

El tamaño del paquete se redujo a 30.1 KB.

Cómo limitar la lista de navegadores compatibles

La cantidad de navegadores de destino incluidos sigue siendo bastante grande, y no muchos usuarios usan navegadores descontinuados, como Internet Explorer. Actualiza los parámetros de configuración con los siguientes valores:

{
  "presets": [
    [
      "@babel/preset-env",
      {
        "targets": "last 2 versions",
        "targets": [">0.25%", "not ie 11"],
        "debug": true,
        "useBuiltIns": "usage",
      }
    ]
  ]
}

Echa un vistazo a los detalles del paquete recuperado.

Tamaño del paquete de 30.0 KB

Como la aplicación es tan pequeña, no hay mucha diferencia con estos cambios. Sin embargo, el enfoque recomendado es usar un porcentaje de participación de mercado del navegador (como ">0.25%") y, al mismo tiempo, excluir los navegadores específicos que tengas la certeza de que tus usuarios no usan. Consulta el artículo "Last 2 versions" considered harmful de James Kyle para obtener más información al respecto.

Usa <script type="module">

Aún hay margen de mejora. Aunque se quitaron varios polyfills sin usar, hay muchos que se envían y que no son necesarios para algunos navegadores. Con los módulos, se puede escribir una sintaxis más nueva y enviarla directamente a los navegadores sin usar polyfills innecesarios.

Los módulos de JavaScript son una función relativamente nueva que se admite en todos los navegadores principales. Los módulos se pueden crear con un atributo type="module" para definir secuencias de comandos que importan y exportan desde otros módulos. Por ejemplo:

// math.mjs
export const add = (x, y) => x + y;

<!-- index.html -->
<script type="module">
  import { add } from './math.mjs';

  add(5, 2); // 7
</script>

Muchas de las funciones más nuevas de ECMAScript ya son compatibles con los entornos que admiten módulos de JavaScript (en lugar de necesitar Babel). Esto significa que la configuración de Babel se puede modificar para enviar dos versiones diferentes de tu aplicación al navegador:

  • Una versión que funcionaría en navegadores más nuevos que admiten módulos y que incluye un módulo que, en gran medida, no se transpila, pero tiene un tamaño de archivo más pequeño
  • Una versión que incluye una secuencia de comandos transpilada más grande que funcionaría en cualquier navegador heredado

Usa módulos de ES con Babel

Para tener parámetros de configuración de @babel/preset-env independientes para las dos versiones de la aplicación, quita el archivo .babelrc. La configuración de Babel se puede agregar a la configuración de webpack especificando dos formatos de compilación diferentes para cada versión de la aplicación.

Para comenzar, agrega una configuración para la secuencia de comandos heredada 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
}

Observa que, en lugar de usar el valor targets para "@babel/preset-env", se usa esmodules con un valor de false. Esto significa que Babel incluye todas las transformaciones y polyfills necesarios para orientar cada navegador que aún no admite módulos de ES.

Agrega los objetos entry, cssRule y corePlugins al principio del archivo webpack.config.js. Todos estos se comparten entre el módulo y las secuencias de comandos heredadas que se entregan al 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"})
];

Ahora, de manera similar, crea un objeto de configuración para la secuencia de comandos del módulo que se muestra a continuación, en la que se define legacyConfig:

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
}

La diferencia principal aquí es que se usa una extensión de archivo .mjs para el nombre del archivo de salida. Aquí, el valor esmodules se establece en verdadero, lo que significa que el código que se genera en este módulo es una secuencia de comandos más pequeña y menos compilada que no pasa por ninguna transformación en este ejemplo, ya que todas las funciones utilizadas ya son compatibles con los navegadores que admiten módulos.

Al final del archivo, exporta ambas configuraciones en un solo array.

module.exports = [
  legacyConfig, moduleConfig
];

Ahora, esto compila un módulo más pequeño para los navegadores que lo admiten y una secuencia de comandos transpilada más grande para los navegadores más antiguos.

Los navegadores que admiten módulos ignoran las secuencias de comandos con un atributo nomodule. Por el contrario, los navegadores que no admiten módulos ignoran los elementos de secuencia de comandos con type="module". Esto significa que puedes incluir un módulo y un resguardo compilado. Idealmente, las dos versiones de la aplicación deberían estar en index.html, como se muestra a continuación:

<script type="module" src="main.mjs"></script>
<script nomodule src="main.bundle.js"></script>

Los navegadores que admiten módulos recuperan y ejecutan main.mjs, y omiten main.bundle.js.. Los navegadores que no admiten módulos hacen lo contrario.

Es importante tener en cuenta que, a diferencia de las secuencias de comandos normales, las secuencias de comandos de módulos siempre se aplazan de forma predeterminada. Si quieres que la secuencia de comandos nomodule equivalente también se aplace y solo se ejecute después del análisis, deberás agregar el atributo defer:

<script type="module" src="main.mjs"></script>
<script nomodule src="main.bundle.js" defer></script>

Lo último que se debe hacer aquí es agregar los atributos module y nomodule al módulo y a la secuencia de comandos heredada, respectivamente. Importa ScriptExtHtmlWebpackPlugin en la parte superior 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");

Ahora, actualiza el array plugins en las configuraciones para incluir este complemento:

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

Esta configuración del complemento agrega un atributo type="module" para todos los elementos de secuencia de comandos .mjs, así como un atributo nomodule para todos los módulos de secuencia de comandos .js.

Entrega módulos en el documento HTML

Lo último que se debe hacer es generar los elementos de secuencia de comandos heredados y modernos en el archivo HTML. Lamentablemente, el complemento que crea el archivo HTML final, HTMLWebpackPlugin, actualmente no admite el resultado de las secuencias de comandos module y nomodule. Aunque existen soluciones alternativas y complementos independientes creados para resolver este problema, como BabelMultiTargetPlugin y HTMLWebpackMultiBuildPlugin, para este instructivo se usa un enfoque más simple que consiste en agregar el elemento de secuencia de comandos del módulo de forma manual.

Agrega lo siguiente a src/index.js al final del archivo:

    ...
    </form>
    <script type="module" src="main.mjs"></script>
  </body>
</html>

Ahora, carga la aplicación en un navegador que admita módulos, como la versión más reciente de Chrome.

Módulo de 5.2 KB recuperado a través de la red para navegadores más nuevos

Solo se recupera el módulo, con un tamaño de paquete mucho más pequeño, ya que, en gran medida, no se transpila. El navegador ignora por completo el otro elemento de secuencia de comandos.

Si cargas la aplicación en un navegador más antiguo, solo se recuperará la secuencia de comandos transpilada más grande con todos los polyfills y las transformaciones necesarios. Aquí tienes una captura de pantalla de todas las solicitudes realizadas en una versión anterior de Chrome (versión 38).

Se recuperó una secuencia de comandos de 30 KB para navegadores más antiguos

Conclusión

Ahora sabes cómo usar @babel/preset-env para proporcionar solo los polyfills necesarios para los navegadores de destino. También sabes cómo los módulos de JavaScript pueden mejorar aún más el rendimiento enviando dos versiones transpiladas diferentes de una aplicación. Con una comprensión decente de cómo ambas técnicas pueden reducir el tamaño de tu paquete de manera significativa, ¡adelante y optimiza!