Regroupement intelligent : comment servir le code hérité uniquement aux navigateurs hérités
Publié: 2022-03-10Aujourd'hui, un site Web reçoit une grande partie de son trafic de navigateurs persistants, dont la plupart ont un bon support pour ES6 +, de nouvelles normes JavaScript, de nouvelles API de plate-forme Web et des attributs CSS. Cependant, les navigateurs hérités doivent encore être pris en charge dans un avenir proche - leur part d'utilisation est suffisamment importante pour ne pas être ignorée, en fonction de votre base d'utilisateurs.
Un rapide coup d'œil au tableau d'utilisation de caniuse.com révèle que les navigateurs à feuilles persistantes occupent la part du lion du marché des navigateurs - plus de 75 %. Malgré cela, la norme est de préfixer CSS, de transpiler tout notre JavaScript vers ES5 et d'inclure des polyfills pour prendre en charge chaque utilisateur qui nous intéresse.
Bien que cela soit compréhensible dans un contexte historique - le Web a toujours été une question d'amélioration progressive - la question demeure : ralentissons-nous le Web pour la majorité de nos utilisateurs afin de prendre en charge un nombre décroissant de navigateurs hérités ?

Le coût de la prise en charge des navigateurs hérités
Essayons de comprendre comment différentes étapes d'un pipeline de build typique peuvent ajouter du poids à nos ressources frontales :
Transpiler vers ES5
Pour estimer le poids que la transpilation peut ajouter à un bundle JavaScript, j'ai pris quelques bibliothèques JavaScript populaires écrites à l'origine en ES6+ et j'ai comparé leurs tailles de bundle avant et après la transpilation :
Bibliothèque | Taille (ES6 minifié) | Taille (ES5 minifié) | Différence |
---|---|---|---|
TodoMVC | 8.4 Ko | 11 Ko | 24,5 % |
Déplaçable | 53,5 Ko | 77,9 Ko | 31,3 % |
Luxon | 75,4 Ko | 100,3 Ko | 24,8 % |
Vidéo.js | 237,2 Ko | 335,8 Ko | 29,4 % |
PixiJS | 370,8 Ko | 452 Ko | 18% |
En moyenne, les faisceaux non transpilés sont environ 25 % plus petits que ceux qui ont été transpilés jusqu'à ES5. Ce n'est pas surprenant étant donné que ES6 + fournit une manière plus compacte et expressive de représenter la logique équivalente et que la transpilation de certaines de ces fonctionnalités vers ES5 peut nécessiter beaucoup de code.
Polyfills ES6+
Bien que Babel applique bien les transformations syntaxiques à notre code ES6+, les fonctionnalités intégrées introduites dans ES6+, telles que Promise
, Map
et Set
, et les nouvelles méthodes de tableau et de chaîne, doivent encore être remplies. L'ajout de babel-polyfill
tel quel peut ajouter près de 90 Ko à votre bundle minifié.
Polyfills de plateforme Web
Le développement d'applications Web modernes a été simplifié grâce à la disponibilité d'une pléthore de nouvelles API de navigateur. Les plus couramment utilisés sont fetch
, pour demander des ressources, IntersectionObserver
, pour observer efficacement la visibilité des éléments, et la spécification d' URL
, qui facilite la lecture et la manipulation des URL sur le Web.
L'ajout d'un polyfill conforme aux spécifications pour chacune de ces fonctionnalités peut avoir un impact notable sur la taille du bundle.
Préfixe CSS
Enfin, regardons l'impact du préfixage CSS. Bien que les préfixes n'ajoutent pas autant de poids mort aux bundles que les autres transformations de construction, en particulier parce qu'ils se compressent bien lorsqu'ils sont Gzipés, il reste encore quelques économies à réaliser ici.
Bibliothèque | Taille (minifié, préfixé pour les 5 dernières versions du navigateur) | Taille (minifié, préfixé pour la dernière version du navigateur) | Différence |
---|---|---|---|
Amorcer | 159 Ko | 132 Ko | 17% |
Bulma | 184 Ko | 164 Ko | 10,9 % |
Fondation | 139 Ko | 118 Ko | 15,1 % |
Interface utilisateur sémantique | 622 Ko | 569 Ko | 8,5 % |
Un guide pratique pour un code efficace d'expédition
Il est probablement évident où je veux en venir. Si nous tirons parti des pipelines de construction existants pour expédier ces couches de compatibilité uniquement aux navigateurs qui en ont besoin, nous pouvons offrir une expérience plus légère au reste de nos utilisateurs - ceux qui forment une majorité croissante - tout en maintenant la compatibilité avec les anciens navigateurs.

Cette idée n'est pas entièrement nouvelle. Des services tels que Polyfill.io tentent de remplir dynamiquement les environnements de navigateur lors de l'exécution. Mais des approches comme celle-ci souffrent de quelques lacunes :
- La sélection de polyfills est limitée à ceux répertoriés par le service, sauf si vous hébergez et gérez le service vous-même.
- Étant donné que le polyfilling se produit au moment de l'exécution et qu'il s'agit d'une opération bloquante, le temps de chargement de la page peut être considérablement plus élevé pour les utilisateurs d'anciens navigateurs.
- Servir un fichier polyfill personnalisé à chaque utilisateur introduit de l'entropie dans le système, ce qui rend le dépannage plus difficile lorsque les choses tournent mal.
De plus, cela ne résout pas le problème du poids ajouté par la transpilation du code de l'application, qui peut parfois être plus volumineux que les polyfills eux-mêmes.
Voyons comment nous pouvons résoudre toutes les sources de ballonnement que nous avons identifiées jusqu'à présent.
Outils dont nous aurons besoin
- Webpack
Ce sera notre outil de construction, bien que le processus reste similaire à celui des autres outils de construction, comme Parcel et Rollup. - Liste des navigateurs
Avec cela, nous gérerons et définirons les navigateurs que nous aimerions prendre en charge. - Et nous utiliserons certains plugins de support Browserslist .
1. Définir les navigateurs modernes et hérités
Tout d'abord, nous voudrons clarifier ce que nous entendons par navigateurs "modernes" et "hérités". Pour faciliter la maintenance et les tests, il est utile de diviser les navigateurs en deux groupes distincts : ajouter les navigateurs qui nécessitent peu ou pas de polyfilling ou de transpilation à notre liste moderne, et mettre le reste sur notre liste héritée.

Une configuration Browserslist à la racine de votre projet peut stocker ces informations. Les sous-sections "Environnement" peuvent être utilisées pour documenter les deux groupes de navigateurs, comme ceci :
[modern] Firefox >= 53 Edge >= 15 Chrome >= 58 iOS >= 10.1 [legacy] > 1%
La liste donnée ici n'est qu'un exemple et peut être personnalisée et mise à jour en fonction des besoins de votre site Web et du temps disponible. Cette configuration servira de source de vérité pour les deux ensembles de bundles frontaux que nous créerons ensuite : un pour les navigateurs modernes et un pour tous les autres utilisateurs.

2. Transpilage et polyremplissage ES6+
Pour transpiler notre JavaScript de manière respectueuse de l'environnement, nous allons utiliser babel-preset-env
.
Initialisons un fichier .babelrc
à la racine de notre projet avec ceci :
{ "presets": [ ["env", { "useBuiltIns": "entry"}] ] }
L'activation de l'indicateur useBuiltIns
permet à Babel de remplir sélectivement les fonctionnalités intégrées qui ont été introduites dans le cadre d'ES6+. Parce qu'il filtre les polyfills pour n'inclure que ceux requis par l'environnement, nous réduisons le coût d'expédition avec babel-polyfill
dans son intégralité.
Pour que ce drapeau fonctionne, nous devrons également importer babel-polyfill
dans notre point d'entrée.
// In import "babel-polyfill";
Cela remplacera la grande importation babel-polyfill
par des importations granulaires, filtrées par l'environnement du navigateur que nous ciblons.
// 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. Fonctionnalités de la plateforme Web de polyremplissage
Pour expédier des polyfills pour les fonctionnalités de la plateforme Web à nos utilisateurs, nous devrons créer deux points d'entrée pour les deux environnements :
require('whatwg-fetch'); require('es6-promise').polyfill(); // … other polyfills
Et ça:
// polyfills for modern browsers (if any) require('intersection-observer');
Il s'agit de la seule étape de notre flux qui nécessite un certain degré de maintenance manuelle. Nous pouvons rendre ce processus moins sujet aux erreurs en ajoutant eslint-plugin-compat au projet. Ce plugin nous avertit lorsque nous utilisons une fonctionnalité du navigateur qui n'a pas encore été remplie.
4. Préfixe CSS
Enfin, voyons comment nous pouvons réduire les préfixes CSS pour les navigateurs qui n'en ont pas besoin. Parce que autoprefixer
a été l'un des premiers outils de l'écosystème à prendre en charge la lecture à partir d'un fichier de configuration browserslist
, nous n'avons pas grand-chose à faire ici.
Créer un simple fichier de configuration PostCSS à la racine du projet devrait suffire :
module.exports = { plugins: [ require('autoprefixer') ], }
Mettre tous ensemble
Maintenant que nous avons défini toutes les configurations de plug-in requises, nous pouvons créer une configuration Webpack qui les lit et génère deux versions distinctes dans les dossiers dist/modern
et 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', }), }, };
Pour finir, nous allons créer quelques commandes de compilation dans notre fichier 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" }
C'est ça. L'exécution de yarn build
devrait maintenant nous donner deux versions, qui sont équivalentes en fonctionnalités.
Servir le bon bundle aux utilisateurs
La création de builds séparés nous aide à atteindre seulement la première moitié de notre objectif. Nous devons encore identifier et proposer le bon bundle aux utilisateurs.
Vous souvenez-vous de la configuration Browserslist que nous avons définie précédemment ? Ne serait-il pas agréable de pouvoir utiliser la même configuration pour déterminer à quelle catégorie appartient l'utilisateur ?
Entrez browserslist-useragent. Comme son nom l'indique, browserslist-useragent
peut lire la configuration de notre liste de browserslist
, puis faire correspondre un agent utilisateur à l'environnement concerné. L'exemple suivant le montre avec un serveur 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); });
Ici, la définition de l'indicateur allowHigherVersions
garantit que si de nouvelles versions d'un navigateur sont publiées - celles qui ne font pas encore partie de la base de données de Can I Use - elles seront toujours signalées comme véridiques pour les navigateurs modernes.
L'une des fonctions de browserslist-useragent
est de s'assurer que les bizarreries de la plate-forme sont prises en compte lors de la mise en correspondance des agents utilisateurs. Par exemple, tous les navigateurs sur iOS (y compris Chrome) utilisent WebKit comme moteur sous-jacent et seront mis en correspondance avec la requête Browserslist spécifique à Safari.
Il n'est peut-être pas prudent de se fier uniquement à l'exactitude de l'analyse de l'agent utilisateur en production. En revenant au bundle hérité pour les navigateurs qui ne sont pas définis dans la liste moderne ou qui ont des chaînes d'agent utilisateur inconnues ou non analysables, nous nous assurons que notre site Web fonctionne toujours.
Bilan : est-ce que ça vaut le coup ?
Nous avons réussi à couvrir un flux de bout en bout pour expédier des offres groupées sans ballonnement à nos clients. Mais il est raisonnable de se demander si les frais généraux de maintenance que cela ajoute à un projet valent ses avantages. Évaluons les avantages et les inconvénients de cette approche :
1. Entretien et test
Il est nécessaire de maintenir une seule configuration Browserslist qui alimente tous les outils de ce pipeline. La mise à jour des définitions des navigateurs modernes et hérités peut être effectuée à tout moment dans le futur sans avoir à refactoriser les configurations ou le code de prise en charge. Je dirais que cela rend les frais généraux de maintenance presque négligeables.
Il existe cependant un petit risque théorique associé au fait de s'appuyer sur Babel pour produire deux ensembles de codes différents, chacun devant fonctionner correctement dans son environnement respectif.
Bien que les erreurs dues aux différences entre les bundles puissent être rares, la surveillance de ces variantes pour les erreurs devrait aider à identifier et à atténuer efficacement les problèmes.
2. Temps de construction vs temps d'exécution
Contrairement à d'autres techniques répandues aujourd'hui, toutes ces optimisations se produisent au moment de la construction et sont invisibles pour le client.
3. Vitesse progressivement améliorée
L'expérience des utilisateurs sur les navigateurs modernes devient nettement plus rapide, tandis que les utilisateurs des anciens navigateurs continuent de bénéficier du même forfait qu'auparavant, sans aucune conséquence négative.
4. Utilisation facile des fonctionnalités du navigateur moderne
Nous évitons souvent d'utiliser les nouvelles fonctionnalités du navigateur en raison de la taille des polyfills nécessaires pour les utiliser. Parfois, nous choisissons même des polyfills plus petits non conformes aux spécifications pour économiser sur la taille. Cette nouvelle approche nous permet d'utiliser des polyfills conformes aux spécifications sans trop se soucier d'affecter tous les utilisateurs.
Faisceau différentiel servant en production
Compte tenu des avantages significatifs, nous avons adopté ce pipeline de construction lors de la création d'une nouvelle expérience de paiement mobile pour les clients d'Urban Ladder, l'un des plus grands détaillants de meubles et de décoration en Inde.
Dans notre offre déjà optimisée, nous avons pu réaliser des économies d'environ 20 % sur les ressources CSS et JavaScript de Gzip envoyées par câble aux utilisateurs mobiles modernes. Étant donné que plus de 80 % de nos visiteurs quotidiens étaient sur ces navigateurs à feuilles persistantes, l'effort fourni en valait bien l'impact.
Autres ressources
- "Charger des polyfills uniquement lorsque cela est nécessaire", Philip Walton
-
@babel/preset-env
Un préréglage Babel intelligent - Liste des navigateurs "Outils"
Écosystème de plugins construit pour Browserslist - Puis-je utiliser
Tableau actuel des parts de marché du navigateur