Empaquetado inteligente: cómo servir código heredado solo a navegadores heredados
Publicado: 2022-03-10Hoy en día, un sitio web recibe una gran parte de su tráfico de navegadores perennes, la mayoría de los cuales tienen un buen soporte para ES6+, nuevos estándares JavaScript, nuevas API de plataforma web y atributos CSS. Sin embargo, los navegadores heredados aún deben ser compatibles en el futuro cercano: su porcentaje de uso es lo suficientemente grande como para no ignorarlo, según su base de usuarios.
Una mirada rápida a la tabla de uso de caniuse.com revela que los navegadores perennes ocupan la mayor parte del mercado de navegadores: más del 75%. A pesar de esto, la norma es prefijar CSS, transpilar todo nuestro JavaScript a ES5 e incluir polyfills para admitir a todos los usuarios que nos interesan.
Si bien esto es comprensible desde un contexto histórico (la web siempre se ha tratado de mejoras progresivas), la pregunta sigue siendo: ¿estamos ralentizando la web para la mayoría de nuestros usuarios a fin de admitir un conjunto cada vez menor de navegadores heredados?
El costo de admitir navegadores heredados
Intentemos comprender cómo los diferentes pasos en una canalización de compilación típica pueden agregar peso a nuestros recursos de front-end:
Transpilar a ES5
Para estimar cuánto peso puede agregar la transpilación a un paquete de JavaScript, tomé algunas bibliotecas de JavaScript populares escritas originalmente en ES6+ y comparé los tamaños de sus paquetes antes y después de la transpilación:
Biblioteca | Tamaño (ES6 minificado) | Tamaño (ES5 minificado) | Diferencia |
---|---|---|---|
TodoMVC | 8.4KB | 11 KB | 24,5% |
Arrastrable | 53.5KB | 77.9KB | 31,3% |
luxon | 75.4KB | 100.3KB | 24,8% |
Vídeo.js | 237.2KB | 335.8KB | 29,4% |
PixiJS | 370.8KB | 452KB | 18% |
En promedio, los paquetes no transpilados son aproximadamente un 25 % más pequeños que los que se han transpilado hasta ES5. Esto no es sorprendente dado que ES6+ proporciona una forma más compacta y expresiva de representar la lógica equivalente y que la transpilación de algunas de estas características a ES5 puede requerir mucho código.
ES6+ Polyfills
Si bien Babel hace un buen trabajo al aplicar transformaciones sintácticas a nuestro código ES6+, las características integradas introducidas en ES6+, como Promise
, Map
and Set
, y los nuevos métodos de matriz y cadena, aún deben polillenarse. Colocar babel-polyfill
tal como está puede agregar cerca de 90 KB a su paquete minificado.
Plataforma Web Polyfills
El desarrollo de aplicaciones web modernas se ha simplificado debido a la disponibilidad de una gran cantidad de nuevas API de navegador. Los más utilizados son fetch
, para solicitar recursos, IntersectionObserver
, para observar de manera eficiente la visibilidad de los elementos, y la especificación de URL
, que facilita la lectura y manipulación de URL en la web.
Agregar un polyfill que cumpla con las especificaciones para cada una de estas características puede tener un impacto notable en el tamaño del paquete.
Prefijo CSS
Por último, veamos el impacto de los prefijos CSS. Si bien los prefijos no agregarán tanto peso muerto a los paquetes como lo hacen otras transformaciones de compilación, especialmente porque se comprimen bien cuando se comprimen con Gzip, todavía se pueden lograr algunos ahorros aquí.
Biblioteca | Tamaño (minificado, prefijado para las últimas 5 versiones del navegador) | Tamaño (minificado, prefijado para la última versión del navegador) | Diferencia |
---|---|---|---|
Oreja | 159 KB | 132 KB | 17% |
Bulma | 184 KB | 164 KB | 10,9% |
Fundación | 139 KB | 118 KB | 15,1% |
IU semántica | 622KB | 569KB | 8,5% |
Una guía práctica para el código eficiente de envío
Probablemente sea evidente a dónde voy con esto. Si aprovechamos las canalizaciones de compilación existentes para enviar estas capas de compatibilidad solo a los navegadores que lo requieran, podemos brindar una experiencia más ligera al resto de nuestros usuarios, aquellos que forman una mayoría creciente, mientras mantenemos la compatibilidad con los navegadores más antiguos.
Esta idea no es del todo nueva. Los servicios como Polyfill.io son intentos de polillenar dinámicamente los entornos del navegador en tiempo de ejecución. Pero enfoques como este adolecen de algunas deficiencias:
- La selección de polyfills se limita a los enumerados por el servicio, a menos que usted mismo aloje y mantenga el servicio.
- Debido a que el polirrelleno ocurre en tiempo de ejecución y es una operación de bloqueo, el tiempo de carga de la página puede ser significativamente mayor para los usuarios de navegadores antiguos.
- Entregar un archivo polyfill personalizado a cada usuario introduce entropía en el sistema, lo que dificulta la resolución de problemas cuando las cosas van mal.
Además, esto no resuelve el problema del peso agregado por la transpilación del código de la aplicación, que a veces puede ser más grande que los mismos polyfills.
Veamos cómo podemos resolver todas las fuentes de hinchazón que hemos identificado hasta ahora.
Herramientas que necesitaremos
- paquete web
Esta será nuestra herramienta de compilación, aunque el proceso seguirá siendo similar al de otras herramientas de compilación, como Parcel y Rollup. - lista de navegadores
Con esto, administraremos y definiremos los navegadores que nos gustaría admitir. - Y usaremos algunos complementos de soporte de Browserslist .
1. Definición de navegadores antiguos y modernos
En primer lugar, queremos aclarar lo que queremos decir con navegadores "modernos" y "heredados". Para facilitar el mantenimiento y las pruebas, es útil dividir los navegadores en dos grupos discretos: agregar navegadores que requieren poca o ninguna transpilación a nuestra lista moderna y colocar el resto en nuestra lista heredada.
Una configuración de lista de navegadores en la raíz de su proyecto puede almacenar esta información. Las subsecciones de "Entorno" se pueden usar para documentar los dos grupos de navegadores, así:
[modern] Firefox >= 53 Edge >= 15 Chrome >= 58 iOS >= 10.1 [legacy] > 1%
La lista que se proporciona aquí es solo un ejemplo y puede personalizarse y actualizarse según los requisitos de su sitio web y el tiempo disponible. Esta configuración actuará como la fuente de verdad para los dos conjuntos de paquetes front-end que crearemos a continuación: uno para los navegadores modernos y otro para todos los demás usuarios.
2. ES6+ Transpiling y Polyfilling
Para transpilar nuestro JavaScript de una manera consciente del entorno, vamos a usar babel-preset-env
.
Inicialicemos un archivo .babelrc
en la raíz de nuestro proyecto con esto:
{ "presets": [ ["env", { "useBuiltIns": "entry"}] ] }
Habilitar el indicador useBuiltIns
le permite a Babel rellenar selectivamente las funciones integradas que se introdujeron como parte de ES6+. Debido a que filtra los polyfills para incluir solo los requeridos por el medio ambiente, mitigamos el costo de envío con babel-polyfill
en su totalidad.
Para que esta bandera funcione, también necesitaremos importar babel-polyfill
en nuestro punto de entrada.
// In import "babel-polyfill";
Al hacerlo, se reemplazará la gran importación babel-polyfill
con importaciones granulares, filtradas por el entorno del navegador al que nos dirigimos.
// Transformed output import "core-js/modules/es7.string.pad-start"; import "core-js/modules/es7.string.pad-end"; import "core-js/modules/web.timers"; …
3. Características de la plataforma web de polirelleno
Para enviar polyfills para las características de la plataforma web a nuestros usuarios, necesitaremos crear dos puntos de entrada para ambos entornos:
require('whatwg-fetch'); require('es6-promise').polyfill(); // … other polyfills
Y esto:
// polyfills for modern browsers (if any) require('intersection-observer');
Este es el único paso en nuestro flujo que requiere cierto grado de mantenimiento manual. Podemos hacer que este proceso sea menos propenso a errores agregando eslint-plugin-compat al proyecto. Este complemento nos advierte cuando usamos una función del navegador que aún no ha sido polillenada.
4. Prefijo CSS
Finalmente, veamos cómo podemos reducir los prefijos CSS para los navegadores que no lo requieren. Debido a que autoprefixer
fue una de las primeras herramientas en el ecosistema en admitir la lectura de un archivo de configuración de lista de browserslist
, no tenemos mucho que hacer aquí.
La creación de un archivo de configuración de PostCSS simple en la raíz del proyecto debería ser suficiente:
module.exports = { plugins: [ require('autoprefixer') ], }
Poniendolo todo junto
Ahora que hemos definido todas las configuraciones de complementos requeridas, podemos armar una configuración de paquete web que los lea y genere dos compilaciones separadas en las carpetas dist/modern
y dist/legacy
.
const MiniCssExtractPlugin = require('mini-css-extract-plugin') const isModern = process.env.BROWSERSLIST_ENV === 'modern' const buildRoot = path.resolve(__dirname, "dist") module.exports = { entry: [ isModern ? './polyfills.modern.js' : './polyfills.legacy.js', "./main.js" ], output: { path: path.join(buildRoot, isModern ? 'modern' : 'legacy'), filename: 'bundle.[hash].js', }, module: { rules: [ { test: /\.jsx?$/, use: "babel-loader" }, { test: /\.css$/, use: [MiniCssExtractPlugin.loader, 'css-loader', 'postcss-loader'] } ]}, plugins: { new MiniCssExtractPlugin(), new HtmlWebpackPlugin({ template: 'index.hbs', filename: 'index.html', }), }, };
Para terminar, crearemos algunos comandos de compilación en nuestro archivo package.json
:
"scripts": { "build": "yarn build:legacy && yarn build:modern", "build:legacy": "BROWSERSLIST_ENV=legacy webpack -p --config webpack.config.js", "build:modern": "BROWSERSLIST_ENV=modern webpack -p --config webpack.config.js" }
Eso es todo. Ejecutar la yarn build
ahora debería darnos dos compilaciones, que son equivalentes en funcionalidad.
Sirviendo el paquete correcto a los usuarios
La creación de compilaciones separadas nos ayuda a lograr solo la primera mitad de nuestro objetivo. Todavía tenemos que identificar y ofrecer el paquete correcto a los usuarios.
¿Recuerda la configuración de la lista de navegadores que definimos anteriormente? ¿No sería bueno si pudiéramos usar la misma configuración para determinar en qué categoría cae el usuario?
Ingrese browserslist-useragent. Como sugiere el nombre, browserslist-useragent
puede leer la configuración de nuestra lista de browserslist
y luego hacer coincidir un agente de usuario con el entorno relevante. El siguiente ejemplo demuestra esto con un servidor Koa:
const Koa = require('koa') const app = new Koa() const send = require('koa-send') const { matchesUA } = require('browserslist-useragent') var router = new Router() app.use(router.routes()) router.get('/', async (ctx, next) => { const useragent = ctx.get('User-Agent') const isModernUser = matchesUA(useragent, { env: 'modern', allowHigherVersions: true, }) const index = isModernUser ? 'dist/modern/index.html', 'dist/legacy/index.html' await send(ctx, index); });
Aquí, configurar el indicador allowHigherVersions
garantiza que si se lanzan versiones más nuevas de un navegador, las que aún no forman parte de la base de datos de Can I Use, aún se informarán como veraces para los navegadores modernos.
Una de las funciones de browserslist-useragent
es garantizar que las peculiaridades de la plataforma se tengan en cuenta al hacer coincidir los agentes de usuario. Por ejemplo, todos los navegadores en iOS (incluido Chrome) utilizan WebKit como motor subyacente y se compararán con la consulta de la lista de navegadores específica de Safari.
Puede que no sea prudente confiar únicamente en la corrección del análisis del agente de usuario en producción. Al recurrir al paquete heredado para los navegadores que no están definidos en la lista moderna o que tienen cadenas de agentes de usuario desconocidas o que no se pueden analizar, nos aseguramos de que nuestro sitio web siga funcionando.
Conclusión: ¿Vale la pena?
Hemos logrado cubrir un flujo de extremo a extremo para enviar paquetes sin problemas a nuestros clientes. Pero es razonable preguntarse si los gastos generales de mantenimiento que esto agrega a un proyecto valen sus beneficios. Vamos a evaluar los pros y los contras de este enfoque:
1. Mantenimiento y pruebas
Se requiere uno para mantener una sola configuración de lista de navegadores que impulse todas las herramientas en esta canalización. La actualización de las definiciones de los navegadores modernos y heredados se puede realizar en cualquier momento en el futuro sin tener que refactorizar las configuraciones o el código de soporte. Yo diría que esto hace que los gastos generales de mantenimiento sean casi insignificantes.
Sin embargo, existe un pequeño riesgo teórico asociado con confiar en Babel para producir dos paquetes de código diferentes, cada uno de los cuales debe funcionar bien en su entorno respectivo.
Si bien los errores debido a las diferencias en los paquetes pueden ser raros, monitorear estas variantes en busca de errores debería ayudar a identificar y mitigar de manera efectiva cualquier problema.
2. Tiempo de compilación frente a tiempo de ejecución
A diferencia de otras técnicas que prevalecen en la actualidad, todas estas optimizaciones ocurren en el momento de la compilación y son invisibles para el cliente.
3. Velocidad progresivamente mejorada
La experiencia de los usuarios en los navegadores modernos se vuelve significativamente más rápida, mientras que los usuarios en los navegadores heredados continúan recibiendo el mismo paquete que antes, sin consecuencias negativas.
4. Uso de las funciones del navegador moderno con facilidad
A menudo evitamos usar nuevas funciones del navegador debido al tamaño de los polyfills necesarios para usarlas. A veces, incluso elegimos polyfills más pequeños que no cumplen con las especificaciones para ahorrar tamaño. Este nuevo enfoque nos permite usar polyfills que cumplen con las especificaciones sin preocuparnos mucho por afectar a todos los usuarios.
Paquete diferencial que sirve en producción
Dadas las importantes ventajas, adoptamos este canal de desarrollo al crear una nueva experiencia de pago móvil para los clientes de Urban Ladder, uno de los minoristas de muebles y decoración más grandes de la India.
En nuestro paquete ya optimizado, pudimos obtener ahorros de aproximadamente un 20 % en los recursos de CSS y JavaScript de Gzip enviados por cable a los usuarios móviles modernos. Debido a que más del 80 % de nuestros visitantes diarios se encontraban en estos navegadores perennes, el esfuerzo realizado valió la pena.
Más recursos
- “Cargar Polyfills solo cuando sea necesario”, Philip Walton
-
@babel/preset-env
Un ajuste preestablecido de Babel inteligente - Lista de navegadores "Herramientas"
Ecosistema de complementos creado para Browserslist - Puedo usar
Tabla actual de cuota de mercado de navegadores