Empaquetado inteligente: cómo servir código heredado solo a navegadores heredados

Publicado: 2022-03-10
Resumen rápido ↬ Si bien la agrupación efectiva de recursos en la web ha recibido una gran cantidad de opiniones en los últimos tiempos, la forma en que enviamos los recursos de front-end a nuestros usuarios se ha mantenido prácticamente igual. El peso promedio de JavaScript y los recursos de estilo con los que se envía un sitio web está aumentando, a pesar de que las herramientas de creación para optimizar el sitio web nunca han sido mejores. Con la participación de mercado de los navegadores perennes aumentando rápidamente y los navegadores lanzando soporte para nuevas funciones al mismo tiempo, ¿es hora de repensar la entrega de activos para la web moderna?

Hoy 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?

Transpilación a ES5, polyfills de plataforma web, polyfills ES6+, prefijo CSS
Las diferentes capas de compatibilidad de una aplicación web. (Ver versión grande)

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.

¡Más después del salto! Continúe leyendo a continuación ↓

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.

El paquete moderno es más pequeño que el paquete heredado porque renuncia a algunas capas de compatibilidad.
Bifurcando nuestros paquetes. (Ver versión grande)

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.

Firefox >= 53; Borde >= 15; cromo >= 58; iOS >= 10.1
Navegadores compatibles con ES6+, nuevos atributos CSS y API de navegador como Promises y Fetch. (Ver versión grande)

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