Intelligente Bündelung: So stellen Sie Legacy-Code nur für Legacy-Browser bereit

Veröffentlicht: 2022-03-10
Kurze Zusammenfassung ↬ Während die effektive Bündelung von Ressourcen im Web in letzter Zeit viel Aufmerksamkeit erregt hat, ist die Art und Weise, wie wir Front-End-Ressourcen an unsere Benutzer versenden, ziemlich gleich geblieben. Das durchschnittliche Gewicht von JavaScript- und Stilressourcen, mit denen eine Website ausgeliefert wird, steigt – obwohl das Build-Tooling zur Optimierung der Website noch nie so gut war. Angesichts des schnell steigenden Marktanteils immergrüner Browser und der Unterstützung neuer Funktionen durch Browser im Gleichschritt ist es an der Zeit, die Bereitstellung von Assets für das moderne Web zu überdenken?

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

Transpilation auf ES5, Webplattform-Polyfills, ES6+-Polyfills, CSS-Präfixierung
Die verschiedenen Kompatibilitätsebenen einer Web-App. (Große Version anzeigen)

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.

Mehr nach dem Sprung! Lesen Sie unten weiter ↓

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.

Das moderne Bundle ist kleiner als das Legacy-Bundle, da es auf einige Kompatibilitätsebenen verzichtet.
Forking unsere Bündel. (Große Version anzeigen)

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.

Firefox >= 53; Kante >= 15; Chrom >= 58; iOS >= 10.1
Browser, die ES6+, neue CSS-Attribute und Browser-APIs wie Promises und Fetch unterstützen. (Große Version anzeigen)

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