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 simple que permite a los usuarios calificar gatos aleatorios. Aprende a optimizar el paquete de JavaScript minimizando cuánto código se transpila.

Captura de pantalla de la app

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 actual del gato.

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 Ver app. Luego, presiona Pantalla completa pantalla completa.
  2. Presiona `Control + Mayúsculas + J` (o `Command + Option + J` en Mac) para abrir Herramientas para desarrolladores.
  3. Haga clic en la pestaña Red.
  4. Selecciona la casilla de verificación Inhabilitar caché.
  5. Vuelve a cargar la app.

Solicitud de tamaño original del paquete

Se usan más de 80 KB para esta aplicación. Es momento de averiguar si partes del paquete no se usan:

  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 Cobertura, haz clic en Volver a cargar para volver a cargar la aplicación mientras capturas la cobertura.

    Vuelve a cargar la app con cobertura de código

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

    Cobertura de código del paquete

Ni siquiera se usa más de la mitad del paquete (44 KB). Esto se debe a que una gran parte del código consta de polyfills para garantizar que la aplicación funcione en navegadores más antiguos.

Usa @babel/preset-env

La sintaxis del lenguaje JavaScript cumple con 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 principal está siempre 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:

Puedes explorar 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 utilizada para compilar código con sintaxis más nueva en código que los navegadores y entornos más antiguos pueden entender. Lo hace de dos maneras:

  • Se incluyen Polyfills para emular funciones de ES2015+ más recientes, de modo que sus APIs se puedan usar incluso si el navegador no lo admite. Este es un ejemplo de un polyfill del método Array.includes.
  • Los complementos se utilizan para transformar el código ES2015 (o posterior) en una sintaxis ES5 más antigua. 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 están incluidas:

"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. De esta manera, todas las configuraciones de Babel se definen en un .babelrc en la raíz del proyecto.
  • babel-loader incluye Babel en el proceso de compilación del paquete web.

Ahora, observa webpack.config.js para ver cómo se incluye babel-loader como 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 objetivos.

Observa el archivo de configuración de Babel, .babelrc, para ver cómo está incluido:

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

Esta es una configuración de Babel y webpack. Aprende a incluir Babel en tu aplicación si usas un agrupador de módulos diferente al de webpack.

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

El valor "last 2 versions" transpila el código en la aplicación para las últimas dos versiones de cada navegador.

Depuración

Para obtener un panorama completo 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 consulta los registros de estado de Glitch en la parte inferior del editor.

Navegadores orientados

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

Navegadores orientados

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

Babel también registra una lista de complementos de transformación usados:

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 anterior para todos los navegadores de destino.

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

No se agregaron polyfills

Esto se debe a que el @babel/polyfill completo se importa de forma directa.

Cómo cargar polyfills de forma individual

De forma predeterminada, Babel incluye todos los polyfills que se necesitan para un entorno de ES2015+ completo cuando se importa @babel/polyfill a un archivo. Si quieres 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 que se incluyen:

Lista de polyfills importados

Si bien ahora solo se incluyen los polyfills necesarios para "last 2 versions", sigue siendo una lista muy larga. Esto se debe a que todavía se incluyen los polyfills necesarios para los navegadores de destino para todas las funciones nuevas. Cambia el valor del atributo a usage a fin de incluir solo los necesarios para las funciones que se usan en el código.

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

De esta manera, se incluyen automáticamente los polyfills cuando es necesario. 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 aplicación se reduce de forma significativa.

Se redujo el tamaño del paquete a 30.1 KB

Restringir la lista de navegadores compatibles

La cantidad de orientaciones de navegador incluidas sigue siendo bastante grande, y no muchos usuarios utilizan navegadores descontinuados, como Internet Explorer. Actualiza la configuración de la siguiente manera:

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

Consulta los detalles del paquete recuperado.

Tamaño del paquete de 30.0 KB

Como la aplicación es tan pequeña, en realidad no hay mucha diferencia con estos cambios. Sin embargo, se recomienda usar un porcentaje de participación de mercado de los navegadores (como ">0.25%"), además de excluir los navegadores específicos que confíes en que los usuarios no están usando. Consulta el artículo "Últimas 2 versiones" consideradas dañinas por James Kyle para obtener más información al respecto.

Usa <script type="module">

Todavía queda más por mejorar. Si bien se quitaron varios polyfills que no se usan, hay muchos que se envían y que algunos navegadores no necesitan. Si usas módulos, se puede escribir una sintaxis más nueva y enviarla a los navegadores directamente sin usar polyfills innecesarios.

Los módulos de JavaScript son una función relativamente nueva compatible con todos los navegadores principales. Se pueden crear módulos 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 funciones más recientes de ECMAScript ya son compatibles con entornos que admiten módulos de JavaScript (en lugar de necesitar Babel). Eso significa que se puede modificar la configuración de Babel para enviar dos versiones diferentes de tu aplicación al navegador:

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

Cómo usar módulos de ES con Babel

Si deseas tener configuraciones de @babel/preset-env separadas para las dos versiones de la app, quita el archivo .babelrc. Se pueden agregar parámetros de configuración de Babel a la configuración de webpack especificando dos formatos de compilación diferentes para cada versión de la aplicación.

Primero, 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
}

Ten en cuenta 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 apuntar a todos los navegadores que aún no admitan módulos ES.

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

De manera similar, crea un objeto de configuración para la secuencia de comandos del módulo a continuación, donde 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 como nombre del archivo de salida. El valor esmodules se establece como verdadero, lo que significa que el código que se envía a 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, se compila un módulo más pequeño para los navegadores compatibles 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. Lo ideal sería que las dos versiones de la aplicación estén en index.html de la siguiente manera:

<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 e ignoran 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 difieren de forma predeterminada. Si deseas que la secuencia de comandos nomodule equivalente también se aplace y se ejecute solo 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 debes 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 la configuración 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 la secuencia de comandos .mjs, así como un atributo nomodule para todos los módulos de secuencia de comandos .js.

Módulos de entrega en el documento HTML

Lo último que se debe hacer es enviar los elementos de secuencia de comandos heredados y modernos al archivo HTML. Lamentablemente, el complemento que crea el archivo HTML final, HTMLWebpackPlugin, no admite la salida de las secuencias de comandos del módulo y nomodule. Aunque se crearon soluciones alternativas y complementos independientes para resolver este problema, como BabelMultiTargetPlugin y HTMLWebpackMultiBuildPlugin, se utiliza un enfoque más simple para agregar manualmente el elemento de secuencia de comandos del módulo para los fines de este instructivo.

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 por medio 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 debido a que en gran medida no se transpila. El navegador ignora por completo el otro elemento de la secuencia de comandos.

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

Se recuperó la secuencia de comandos de 30 KB para navegadores anteriores

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 mediante el envío de dos versiones transpiladas diferentes de una aplicación. Si comprendes bien cómo estas técnicas pueden reducir de manera significativa el tamaño del paquete, procede a optimizar.