En este codelab, mejorarás el rendimiento de esta aplicación simple que permite a los usuarios calificar gatos aleatorios. Obtén información para optimizar el paquete de JavaScript minimizando la cantidad de código que se transpila.
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:
- Para obtener una vista previa del sitio, presiona Ver app. Luego, presiona Pantalla completa
.
- Presiona "Control + Mayús + J" (o "Comando + Opción + J" en Mac) para abrir Herramientas para desarrolladores.
- Haga clic en la pestaña Red.
- Selecciona la casilla de verificación Inhabilitar caché.
- Vuelve a cargar la app.
Se utilizan más de 80 KB para esta aplicación. Es momento de averiguar si no se usan partes del paquete:
Presiona
Control+Shift+P
(oCommand+Shift+P
en Mac) para abrir el menú Command.Ingresa
Show Coverage
y presionaEnter
para mostrar la pestaña Cobertura.En la pestaña Cobertura, haz clic en Volver a cargar para volver a cargar la aplicación mientras capturas la cobertura.
Observa cuánto código se usó en comparación con cuánto se cargó para el paquete principal:
Ni siquiera se utiliza más de la mitad del paquete (44 KB). Esto se debe a que una gran parte de su código consiste en 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. Todos los años, se lanzan versiones más recientes de la especificación, que incluyen funciones nuevas que pasaron el proceso de propuesta. Cada navegador importante se encuentra siempre en una etapa diferente de compatibilidad para estas funciones.
Las siguientes funciones de ES2015 se usan en la aplicación:
También se utiliza la siguiente función de ES2017:
No dudes en 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 que se usa para compilar código que contiene una sintaxis más reciente en código que los navegadores y entornos más antiguos pueden comprender. Lo hace de dos maneras:
- Los Polyfills se incluyen para emular las funciones ES2015+ más recientes, de modo que sus API 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 ES2015 (o posterior) en una sintaxis ES5 anterior. Dado que se trata de cambios relacionados con la sintaxis (como las funciones de flecha), no se pueden emular con polyfills.
Observa 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. Así, 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 regla:
module: { rules: [ //... { test: /\.js$/, exclude: /node_modules/, loader: "babel-loader" } ] },
@babel/polyfill
proporciona todos los polyfills necesarios para las funciones más recientes de ECMAScript, de modo que puedan funcionar en entornos que no las admitan. Ya se importó en la parte superior desrc/index.js.
import "./style.css";
import "@babel/polyfill";
@babel/preset-env
identifica qué transformaciones y polyfills son necesarias para cualquier navegador o entorno que se elija como objetivos.
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 agrupador de módulos diferente al webpack.
El atributo targets
en .babelrc
identifica a qué navegadores se orientan. @babel/preset-env
se integra en browselist, lo que significa que puedes encontrar una lista completa de consultas compatibles que se pueden usar en este campo en la documentación sobre la lista de navegadores.
El valor "last 2 versions"
transpila el código en la aplicación para las
dos últimas 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 una serie de detalles en la consola sobre el proceso de compilación, incluidos todos los entornos de destino para los que se compiló el código.
Observa cómo los navegadores descontinuados, como Internet Explorer, se incluyen en esta lista. Esto genera un problema porque los navegadores no compatibles no tendrán funciones más nuevas y Babel continúa transpilando su sintaxis específica. 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 complementos de transformación utilizados:
Es una lista bastante larga. Estos son todos los complementos que Babel necesita usar para transformar cualquier sintaxis ES2015+ en una sintaxis anterior para todos los navegadores de destino.
Sin embargo, Babel no muestra polyfills específicos que se usan:
Esto se debe a que se importa todo el @babel/polyfill
de forma directa.
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. 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:
Aunque ahora se incluyen solo los polyfills necesarios para "last 2 versions"
, sigue siendo una lista muy larga. Esto se debe a que aún se incluyen los polyfills necesarios para los navegadores de destino en 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 este modo, los polyfills se incluyen automáticamente 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.
El tamaño del paquete de aplicación se reduce de forma significativa.
Cómo reducir 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 a lo siguiente:
{
"presets": [
[
"@babel/preset-env",
{
"targets": "last 2 versions",
"targets": [">0.25%", "not ie 11"],
"debug": true,
"useBuiltIns": "usage",
}
]
]
}
Consulta los detalles del paquete recuperado.
Como la aplicación es tan pequeña, en realidad no hay mucha diferencia con estos cambios. Sin embargo, el enfoque recomendado es usar un porcentaje de participación de mercado de navegadores (como ">0.25%"
) y excluir navegadores específicos que estés seguro de que tus usuarios no usarán. Consulta el artículo sobre las “últimas 2 versiones” consideradas dañinas de James Kyle para obtener más información sobre este tema.
Cómo usar <script type="module">
Todavía queda mucho por mejorar. Si bien se quitaron varios polyfills sin usar, hay muchos que se envían y que no son necesarios para algunos navegadores. Con el uso de módulos, se puede escribir y enviar la sintaxis más reciente a los navegadores directamente sin el uso de polyfills innecesarios.
Los módulos de JavaScript son una función relativamente nueva compatible con todos los navegadores principales.
Los módulos se pueden crear con un atributo type="module"
para definir secuencias de comandos que se 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 en 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 admitan módulos y que incluya un módulo que no está transpilado, pero que 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
Si quieres tener configuraciones de @babel/preset-env
separadas para las dos versiones de la aplicación, quita el archivo .babelrc
. La configuración de Babel se puede agregar a la configuración del webhook 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 necesarias para segmentarse a todos los navegadores que aún no admiten módulos de ES.
Agrega objetos entry
, cssRule
y corePlugins
al comienzo del archivo webpack.config.js
. Todos estos elementos 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"})
];
De manera similar, crea un objeto de configuración para la secuencia de comandos del módulo a continuación, en el 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 principal diferencia aquí es que se usa una extensión de archivo .mjs
para el nombre de archivo de salida. El valor esmodules
se establece como verdadero aquí, 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, dado que todas las funciones utilizadas ya son compatibles con 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 anteriores.
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 así como 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 compatibles con 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 los módulos siempre se aplazan de forma predeterminada.
Si deseas que también se aplace la secuencia de comandos nomodule
equivalente 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 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 en el archivo HTML. Lamentablemente, el complemento que crea el archivo HTML final, HTMLWebpackPlugin
, actualmente no admite la salida de las secuencias de comandos "module" y "nomodule". Aunque se crean soluciones alternativas y complementos independientes para resolver este problema, como BabelMultiTargetPlugin y HTMLWebpackMultiBuildPlugin, en este instructivo, se utiliza un enfoque más simple para agregar manualmente el elemento de secuencia de comandos del módulo.
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.
Solo se recupera el módulo, con un tamaño de paquete mucho más pequeño, ya que no se transpila en gran medida. 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 muestra una captura de pantalla de todas las solicitudes realizadas en una versión anterior de Chrome (versión 38).
Conclusión
Ahora sabes cómo usar @babel/preset-env
para proporcionar solo los polyfills necesarios que se necesitan para los navegadores objetivo. 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
el tamaño de tus paquetes de manera significativa, comienza a optimizarla.