Intelligente Bündelung: So stellen Sie Legacy-Code nur für Legacy-Browser bereit
Veröffentlicht: 2022-03-10Eine Website erhält heute einen großen Teil ihres Datenverkehrs von immergrünen Browsern – von denen die meisten ES6+, neue JavaScript-Standards, neue Webplattform-APIs und CSS-Attribute gut unterstützen. Legacy-Browser müssen jedoch für die nahe Zukunft noch unterstützt werden – ihr Nutzungsanteil ist groß genug, um je nach Benutzerbasis nicht ignoriert zu werden.
Ein kurzer Blick auf die Nutzungstabelle von caniuse.com zeigt, dass immergrüne Browser einen Löwenanteil des Browsermarktes einnehmen – mehr als 75 %. Trotzdem ist es üblich, CSS voranzustellen, unser gesamtes JavaScript in ES5 zu transpilieren und Polyfills einzuschließen, um jeden Benutzer zu unterstützen, der uns wichtig ist.
Obwohl dies aus historischem Kontext verständlich ist – im Web ging es schon immer um progressive Verbesserung – bleibt die Frage: Verlangsamen wir das Web für die Mehrheit unserer Benutzer, um eine immer weniger werdende Anzahl von Legacy-Browsern zu unterstützen?
Die Kosten für die Unterstützung älterer Browser
Versuchen wir zu verstehen, wie verschiedene Schritte in einer typischen Build-Pipeline unsere Front-End-Ressourcen gewichten können:
Transpilieren nach ES5
Um abzuschätzen, wie viel Gewicht das Transpilieren einem JavaScript-Bundle hinzufügen kann, habe ich einige beliebte JavaScript-Bibliotheken genommen, die ursprünglich in ES6+ geschrieben wurden, und ihre Bundle-Größen vor und nach der Transpilation verglichen:
Bücherei | Größe (minimierter ES6) | Größe (minimierter ES5) | Unterschied |
---|---|---|---|
TodoMVC | 8,4 KB | 11 KB | 24,5 % |
Ziehbar | 53,5 KB | 77,9 KB | 31,3 % |
Luxon | 75,4 KB | 100,3 KB | 24,8 % |
Video.js | 237,2 KB | 335,8 KB | 29,4 % |
PixiJS | 370,8 KB | 452 KB | 18% |
Im Durchschnitt sind nicht transpilierte Bündel etwa 25 % kleiner als diejenigen, die bis auf ES5 transpiliert wurden. Dies ist nicht überraschend, da ES6+ eine kompaktere und ausdrucksstärkere Möglichkeit bietet, die äquivalente Logik darzustellen, und dass die Übertragung einiger dieser Funktionen auf ES5 viel Code erfordern kann.
ES6+ Polyfills
Während Babel bei der Anwendung syntaktischer Transformationen auf unseren ES6+-Code gute Arbeit leistet, müssen integrierte Funktionen, die in ES6+ eingeführt wurden – wie Promise
, Map
und Set
, und neue Array- und String-Methoden – noch polyfilled werden. Wenn Sie babel-polyfill
polyfill unverändert einfügen, können Sie Ihrem minimierten Paket fast 90 KB hinzufügen.
Webplattform-Polyfills
Die Entwicklung moderner Webanwendungen wurde durch die Verfügbarkeit einer Vielzahl neuer Browser-APIs vereinfacht. Häufig verwendete sind fetch
zum Anfordern von Ressourcen, IntersectionObserver
zum effizienten Beobachten der Sichtbarkeit von Elementen und die URL
-Spezifikation, die das Lesen und Bearbeiten von URLs im Web erleichtert.
Das Hinzufügen eines spezifikationskonformen Füllmaterials für jedes dieser Merkmale kann sich spürbar auf die Bündelgröße auswirken.
CSS-Präfixierung
Schauen wir uns zum Schluss die Auswirkungen von CSS-Präfixen an. Während Präfixe Bündeln nicht so viel Eigengewicht hinzufügen werden wie andere Build-Transformationen – insbesondere, weil sie sich gut komprimieren lassen, wenn sie mit Gzip gezippt werden –, gibt es hier noch einige Einsparungen zu erzielen.
Bücherei | Größe (minifiziert, Präfix für die letzten 5 Browserversionen) | Größe (minimiert, vorangestellt für letzte Browserversion) | Unterschied |
---|---|---|---|
Bootstrap | 159 KB | 132 KB | 17% |
Bulma | 184 KB | 164 KB | 10,9 % |
Stiftung | 139 KB | 118 KB | 15,1 % |
Semantische Benutzeroberfläche | 622 KB | 569 KB | 8,5 % |
Ein praktischer Leitfaden für einen effizienten Versandcode
Es ist wahrscheinlich offensichtlich, wohin ich damit gehe. Wenn wir vorhandene Build-Pipelines nutzen, um diese Kompatibilitätsebenen nur an Browser zu liefern, die dies erfordern, können wir dem Rest unserer Benutzer – denjenigen, die eine wachsende Mehrheit bilden – ein leichteres Erlebnis bieten und gleichzeitig die Kompatibilität für ältere Browser aufrechterhalten.
Diese Idee ist nicht ganz neu. Dienste wie Polyfill.io sind Versuche, Browserumgebungen zur Laufzeit dynamisch mit Polyfill zu füllen. Aber Ansätze wie dieser leiden an einigen Mängeln:
- Die Auswahl an Polyfills ist auf die vom Dienst aufgelisteten beschränkt – es sei denn, Sie hosten und warten den Dienst selbst.
- Da das Polyfilling zur Laufzeit erfolgt und ein Blockierungsvorgang ist, kann die Seitenladezeit für Benutzer mit alten Browsern erheblich länger sein.
- Das Bereitstellen einer maßgeschneiderten Polyfill-Datei für jeden Benutzer führt zu Entropie im System, was die Fehlerbehebung erschwert, wenn etwas schief geht.
Außerdem löst dies nicht das Problem des Gewichtes, das durch Transpilation des Anwendungscodes hinzugefügt wird, der manchmal größer sein kann als die Polyfills selbst.
Mal sehen, wie wir alle bisher identifizierten Quellen von Blähungen lösen können.
Werkzeuge, die wir brauchen
- Webpaket
Dies wird unser Build-Tool sein, obwohl der Prozess ähnlich wie bei anderen Build-Tools wie Parcel und Rollup bleibt. - Browserliste
Damit verwalten und definieren wir die Browser, die wir unterstützen möchten. - Und wir werden einige Browserlist-Unterstützungs-Plugins verwenden.
1. Definition moderner und älterer Browser
Zunächst möchten wir klarstellen, was wir mit „modernen“ und „alten“ Browsern meinen. Um die Wartung und das Testen zu vereinfachen, ist es hilfreich, Browser in zwei getrennte Gruppen zu unterteilen: Hinzufügen von Browsern, die wenig bis gar kein Polyfilling oder Transpilation zu unserer modernen Liste erfordern, und Hinzufügen des Rests zu unserer Legacy-Liste.
Eine Browserslist-Konfiguration im Stammverzeichnis Ihres Projekts kann diese Informationen speichern. Die Unterabschnitte „Umgebung“ können verwendet werden, um die beiden Browsergruppen wie folgt zu dokumentieren:
[modern] Firefox >= 53 Edge >= 15 Chrome >= 58 iOS >= 10.1 [legacy] > 1%
Die hier aufgeführte Liste ist nur ein Beispiel und kann je nach den Anforderungen Ihrer Website und der verfügbaren Zeit angepasst und aktualisiert werden. Diese Konfiguration dient als Quelle der Wahrheit für die beiden Front-End-Bundles, die wir als Nächstes erstellen werden: eines für die modernen Browser und eines für alle anderen Benutzer.
2. ES6+ Transpiling und Polyfilling
Um unser JavaScript umweltbewusst zu transpilieren, verwenden wir babel-preset-env
.
Lassen Sie uns damit eine .babelrc
-Datei im Stammverzeichnis unseres Projekts initialisieren:
{ "presets": [ ["env", { "useBuiltIns": "entry"}] ] }
Durch Aktivieren des useBuiltIns
-Flags kann Babel integrierte Funktionen, die als Teil von ES6+ eingeführt wurden, selektiv polyfillen. Da Polyfills so gefiltert werden, dass sie nur diejenigen enthalten, die für die Umwelt erforderlich sind, verringern wir die Versandkosten mit babel-polyfill
vollständig.
Damit dieses Flag funktioniert, müssen wir auch babel-polyfill
in unseren Einstiegspunkt importieren.
// In import "babel-polyfill";
Dadurch wird der große babel-polyfill
Import durch granulare Importe ersetzt, die nach der Browserumgebung gefiltert werden, auf die wir abzielen.
// 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. Funktionen der Polyfilling-Webplattform
Um Polyfills für Webplattformfunktionen an unsere Benutzer zu liefern, müssen wir zwei Einstiegspunkte für beide Umgebungen erstellen:
require('whatwg-fetch'); require('es6-promise').polyfill(); // … other polyfills
Und das:
// polyfills for modern browsers (if any) require('intersection-observer');
Dies ist der einzige Schritt in unserem Ablauf, der ein gewisses Maß an manueller Wartung erfordert. Wir können diesen Prozess weniger fehleranfällig machen, indem wir eslint-plugin-compat zum Projekt hinzufügen. Dieses Plugin warnt uns, wenn wir eine Browserfunktion verwenden, die noch nicht polyfilled ist.
4. CSS-Präfixierung
Lassen Sie uns abschließend sehen, wie wir CSS-Präfixe für Browser reduzieren können, die dies nicht benötigen. Da autoprefixer
eines der ersten Tools im Ökosystem war, das das Lesen aus einer browserslist
-Konfigurationsdatei unterstützte, haben wir hier nicht viel zu tun.
Das Erstellen einer einfachen PostCSS-Konfigurationsdatei im Stammverzeichnis des Projekts sollte ausreichen:
module.exports = { plugins: [ require('autoprefixer') ], }
Alles zusammenfügen
Nachdem wir nun alle erforderlichen Plugin-Konfigurationen definiert haben, können wir eine Webpack-Konfiguration zusammenstellen, die diese liest und zwei separate Builds in den Ordnern dist/modern
und dist/legacy
ausgibt.
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', }), }, };
Zum Abschluss erstellen wir einige Build-Befehle in unserer Datei 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" }
Das ist es. Laufender yarn build
sollte uns jetzt zwei Builds geben, die in der Funktionalität gleichwertig sind.
Bereitstellung des richtigen Pakets für Benutzer
Das Erstellen separater Builds hilft uns, nur die erste Hälfte unseres Ziels zu erreichen. Wir müssen noch das richtige Paket identifizieren und den Benutzern bereitstellen.
Erinnern Sie sich an die Browserslist-Konfiguration, die wir zuvor definiert haben? Wäre es nicht schön, wenn wir die gleiche Konfiguration verwenden könnten, um zu bestimmen, in welche Kategorie der Benutzer fällt?
Geben Sie browserslist-useragent ein. Wie der Name schon sagt, kann browserslist-useragent
unsere browserslist
-Konfiguration lesen und dann einen User-Agent mit der entsprechenden Umgebung abgleichen. Das folgende Beispiel demonstriert dies mit einem Koa-Server:
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); });
Hier stellt das Setzen des allowHigherVersions
sicher, dass, wenn neuere Versionen eines Browsers veröffentlicht werden – solche, die noch nicht Teil der Can I Use-Datenbank sind – sie für moderne Browser immer noch als wahr gemeldet werden.
Eine der Funktionen von browserslist-useragent
besteht darin, sicherzustellen, dass Plattform-Eigenheiten beim Abgleich von Benutzeragenten berücksichtigt werden. Beispielsweise verwenden alle Browser unter iOS (einschließlich Chrome) WebKit als zugrunde liegende Engine und werden mit der jeweiligen Safari-spezifischen Browserlisten-Abfrage abgeglichen.
Es ist möglicherweise nicht ratsam, sich ausschließlich auf die Korrektheit der Benutzeragentenanalyse in der Produktion zu verlassen. Indem wir für Browser, die nicht in der modernen Liste definiert sind oder unbekannte oder nicht parsbare User-Agent-Strings haben, auf das Legacy-Bundle zurückgreifen, stellen wir sicher, dass unsere Website weiterhin funktioniert.
Fazit: Lohnt es sich?
Wir haben es geschafft, einen End-to-End-Fluss für den Versand von aufblähungsfreien Paketen an unsere Kunden abzudecken. Aber es ist nur vernünftig, sich zu fragen, ob der Wartungsaufwand, den dies zu einem Projekt hinzufügt, seinen Nutzen wert ist. Lassen Sie uns die Vor- und Nachteile dieses Ansatzes bewerten:
1. Wartung und Prüfung
Man muss nur eine einzige Browserslist-Konfiguration pflegen, die alle Tools in dieser Pipeline unterstützt. Die Aktualisierung der Definitionen moderner und älterer Browser kann jederzeit in der Zukunft erfolgen, ohne dass unterstützende Konfigurationen oder Code umgestaltet werden müssen. Ich würde argumentieren, dass dies den Wartungsaufwand fast vernachlässigbar macht.
Es besteht jedoch ein kleines theoretisches Risiko, sich auf Babel zu verlassen, um zwei verschiedene Codepakete zu erstellen, von denen jedes in seiner jeweiligen Umgebung gut funktionieren muss.
Auch wenn Fehler aufgrund unterschiedlicher Bundles selten vorkommen, sollte die Überwachung dieser Varianten auf Fehler helfen, Probleme zu identifizieren und effektiv zu mindern.
2. Bauzeit vs. Laufzeit
Im Gegensatz zu anderen heute vorherrschenden Techniken erfolgen alle diese Optimierungen zur Erstellungszeit und sind für den Client unsichtbar.
3. Progressiv erhöhte Geschwindigkeit
Die Erfahrung von Benutzern mit modernen Browsern wird erheblich schneller, während Benutzer mit älteren Browsern weiterhin das gleiche Paket wie zuvor erhalten, ohne negative Folgen.
4. Einfache Verwendung moderner Browserfunktionen
Wir vermeiden häufig die Verwendung neuer Browserfunktionen aufgrund der Größe der Polyfills, die für deren Verwendung erforderlich sind. Manchmal wählen wir sogar kleinere, nicht spezifikationskonforme Polyfills, um Größe zu sparen. Dieser neue Ansatz ermöglicht es uns, spezifikationskonforme Polyfills zu verwenden, ohne uns große Gedanken darüber zu machen, dass alle Benutzer davon betroffen sind.
Differentielle Bündelbereitstellung in der Produktion
Angesichts der erheblichen Vorteile haben wir diese Build-Pipeline übernommen, als wir ein neues mobiles Kassenerlebnis für Kunden von Urban Ladder, einem der größten Möbel- und Einrichtungshändler Indiens, geschaffen haben.
In unserem bereits optimierten Paket konnten wir Einsparungen von etwa 20 % bei den mit Gzip versehenen CSS- und JavaScript-Ressourcen erzielen, die an moderne mobile Benutzer gesendet wurden. Da mehr als 80 % unserer täglichen Besucher diese Evergreen-Browser nutzten, hat sich der Aufwand gelohnt.
Weitere Ressourcen
- „Polyfills nur bei Bedarf laden“, Philip Walton
-
@babel/preset-env
Ein intelligentes Babel-Preset - Browserliste „Tools“
Ökosystem von Plugins, die für Browserslist entwickelt wurden - Kann ich benutzen
Aktuelle Browser-Marktanteilstabelle