Smart Bundling: come fornire codice legacy solo a browser legacy
Pubblicato: 2022-03-10Un sito Web oggi riceve gran parte del suo traffico da browser sempreverdi, la maggior parte dei quali ha un buon supporto per ES6+, nuovi standard JavaScript, nuove API della piattaforma Web e attributi CSS. Tuttavia, i browser legacy devono ancora essere supportati per il prossimo futuro: la loro quota di utilizzo è abbastanza grande da non essere ignorata, a seconda della base di utenti.
Una rapida occhiata alla tabella di utilizzo di caniuse.com rivela che i browser evergreen occupano una parte da leone del mercato dei browser: oltre il 75%. Nonostante ciò, la norma è anteporre CSS, trasporre tutto il nostro JavaScript in ES5 e includere polyfill per supportare ogni utente a cui teniamo.
Sebbene ciò sia comprensibile da un contesto storico - il Web è sempre stato incentrato sul miglioramento progressivo - la domanda rimane: stiamo rallentando il Web per la maggior parte dei nostri utenti al fine di supportare un insieme in diminuzione di browser legacy?

Il costo del supporto dei browser legacy
Proviamo a capire in che modo diversi passaggi in una tipica pipeline di compilazione possono aggiungere peso alle nostre risorse front-end:
Transpilazione a ES5
Per stimare la quantità di peso che il transpiling può aggiungere a un bundle JavaScript, ho preso alcune popolari librerie JavaScript originariamente scritte in ES6+ e ho confrontato le loro dimensioni del bundle prima e dopo la transpilazione:
Biblioteca | Dimensione (ES6 minimizzato) | Dimensione (ES5 minimizzato) | Differenza |
---|---|---|---|
TodoMVC | 8,4 KB | 11 KB | 24,5% |
Trascinabile | 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% |
In media, i bundle non trasferiti sono circa il 25% più piccoli di quelli che sono stati trasferiti a ES5. Ciò non sorprende dato che ES6+ fornisce un modo più compatto ed espressivo per rappresentare la logica equivalente e che la traslazione di alcune di queste funzionalità in ES5 può richiedere molto codice.
ES6+ Polyfill
Sebbene Babel esegua un buon lavoro nell'applicare trasformazioni sintattiche al nostro codice ES6+, le funzionalità integrate introdotte in ES6+, come Promise
, Map
and Set
e nuovi metodi array e string, devono ancora essere riempite in poli. L'eliminazione di babel-polyfill
così com'è può aggiungere quasi 90 KB al tuo pacchetto minimizzato.
Piattaforma Web Polyfills
Lo sviluppo di moderne applicazioni Web è stato semplificato grazie alla disponibilità di una pletora di nuove API del browser. Quelli comunemente usati sono fetch
, per la richiesta di risorse, IntersectionObserver
, per osservare in modo efficiente la visibilità degli elementi, e la specifica degli URL
, che facilita la lettura e la manipolazione degli URL sul web.
L'aggiunta di un polyfill conforme alle specifiche per ciascuna di queste funzionalità può avere un notevole impatto sulle dimensioni del pacchetto.
Prefisso CSS
Infine, diamo un'occhiata all'impatto del prefisso CSS. Anche se i prefissi non aggiungeranno tanto peso morto ai bundle come fanno altre trasformazioni di build, soprattutto perché si comprimono bene quando vengono compressi con Gzip, ci sono ancora alcuni risparmi da ottenere qui.
Biblioteca | Dimensione (ridotto, prefisso per le ultime 5 versioni del browser) | Dimensione (ridotto, prefisso per l'ultima versione del browser) | Differenza |
---|---|---|---|
Bootstrap | 159 KB | 132 KB | 17% |
Bulma | 184 KB | 164 KB | 10,9% |
Fondazione | 139 KB | 118 KB | 15,1% |
Interfaccia semantica | 622 KB | 569 KB | 8,5% |
Una guida pratica al codice efficiente di spedizione
Probabilmente è evidente dove sto andando con questo. Se sfruttiamo le pipeline di build esistenti per spedire questi livelli di compatibilità solo ai browser che lo richiedono, possiamo offrire un'esperienza più leggera al resto dei nostri utenti, quelli che costituiscono una maggioranza in aumento, mantenendo la compatibilità per i browser meno recenti.

Questa idea non è del tutto nuova. Servizi come Polyfill.io sono tentativi di riempimento dinamico degli ambienti del browser in fase di esecuzione. Ma approcci come questo soffrono di alcune carenze:
- La selezione di polyfill è limitata a quelli elencati dal servizio, a meno che tu non ospiti e gestisca personalmente il servizio.
- Poiché il polyfilling avviene in fase di esecuzione ed è un'operazione di blocco, il tempo di caricamento della pagina può essere significativamente più elevato per gli utenti che utilizzano vecchi browser.
- Fornire un file polyfill personalizzato a ogni utente introduce entropia nel sistema, il che rende più difficile la risoluzione dei problemi quando le cose vanno male.
Inoltre, questo non risolve il problema del peso aggiunto dalla traspirazione del codice dell'applicazione, che a volte può essere più grande dei polyfill stessi.
Vediamo come possiamo risolvere tutte le fonti di gonfiore che abbiamo identificato fino ad ora.
Strumenti di cui avremo bisogno
- Pacchetto Web
Questo sarà il nostro strumento di compilazione, anche se il processo rimarrà simile a quello di altri strumenti di compilazione, come Parcel e Rollup. - Elenco browser
Con questo, gestiremo e definiremo i browser che vorremmo supportare. - E utilizzeremo alcuni plug-in di supporto di Browserslist .
1. Definizione di browser moderni e legacy
Innanzitutto, vorremo chiarire cosa intendiamo per browser "moderni" e "legacy". Per facilità di manutenzione e test, è utile dividere i browser in due gruppi distinti: aggiungere browser che richiedono poco o nessun polyfilling o transpilation al nostro elenco moderno e inserire il resto nel nostro elenco legacy.

Una configurazione Browserslist nella radice del tuo progetto può memorizzare queste informazioni. Le sottosezioni "Ambiente" possono essere utilizzate per documentare i due gruppi di browser, in questo modo:
[modern] Firefox >= 53 Edge >= 15 Chrome >= 58 iOS >= 10.1 [legacy] > 1%
L'elenco qui riportato è solo un esempio e può essere personalizzato e aggiornato in base alle esigenze del tuo sito web e al tempo a disposizione. Questa configurazione fungerà da fonte di verità per i due set di bundle front-end che creeremo in seguito: uno per i browser moderni e uno per tutti gli altri utenti.

2. ES6+ Transpiling e Polyfilling
Per trasferire il nostro JavaScript in modo consapevole dell'ambiente, utilizzeremo babel-preset-env
.
Inizializziamo un file .babelrc
alla radice del nostro progetto con questo:
{ "presets": [ ["env", { "useBuiltIns": "entry"}] ] }
L'abilitazione del flag useBuiltIns
consente a Babel di riempire selettivamente le funzionalità integrate introdotte come parte di ES6+. Poiché filtra i polyfill per includere solo quelli richiesti dall'ambiente, riduciamo il costo della spedizione con babel-polyfill
nella sua interezza.
Affinché questo flag funzioni, dovremo anche importare babel-polyfill
nel nostro punto di ingresso.
// In import "babel-polyfill";
In questo modo sostituirai la grande importazione babel-polyfill
con importazioni granulari, filtrate dall'ambiente del browser che stiamo prendendo di mira.
// 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. Funzionalità della piattaforma Web Polyfilling
Per spedire i polyfill per le funzionalità della piattaforma web ai nostri utenti, dovremo creare due punti di ingresso per entrambi gli ambienti:
require('whatwg-fetch'); require('es6-promise').polyfill(); // … other polyfills
E questo:
// polyfills for modern browsers (if any) require('intersection-observer');
Questo è l'unico passaggio nel nostro flusso che richiede un certo grado di manutenzione manuale. Possiamo rendere questo processo meno soggetto a errori aggiungendo eslint-plugin-compat al progetto. Questo plugin ci avverte quando utilizziamo una funzione del browser che non è stata ancora compilata in polyfill.
4. Prefisso CSS
Infine, vediamo come possiamo ridurre i prefissi CSS per i browser che non lo richiedono. Poiché autoprefixer
è stato uno dei primi strumenti nell'ecosistema a supportare la lettura da un file di configurazione browserslist
, non abbiamo molto da fare qui.
La creazione di un semplice file di configurazione PostCSS alla radice del progetto dovrebbe essere sufficiente:
module.exports = { plugins: [ require('autoprefixer') ], }
Mettere tutto insieme
Ora che abbiamo definito tutte le configurazioni dei plugin richieste, possiamo mettere insieme una configurazione webpack che le legge e genera due build separate nelle cartelle dist/modern
e 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', }), }, };
Per finire, creeremo alcuni comandi di build nel nostro file 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" }
Questo è tutto. L'esecuzione della yarn build
ora dovrebbe darci due build, che sono equivalenti in termini di funzionalità.
Servire il pacchetto giusto per gli utenti
La creazione di build separate ci aiuta a raggiungere solo la prima metà del nostro obiettivo. Dobbiamo ancora identificare e fornire agli utenti il pacchetto giusto.
Ricordi la configurazione Browserslist che abbiamo definito in precedenza? Non sarebbe bello se potessimo utilizzare la stessa configurazione per determinare in quale categoria rientra l'utente?
Immettere browserslist-useragent. Come suggerisce il nome, browserslist-useragent
può leggere la nostra configurazione browserslist
e quindi abbinare un user agent all'ambiente pertinente. L'esempio seguente lo dimostra con un server 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); });
Qui, l'impostazione del flag allowHigherVersions
garantisce che se vengono rilasciate versioni più recenti di un browser, quelle che non fanno ancora parte del database di Can I Use, verranno comunque riportate come veritiere per i browser moderni.
Una delle funzioni di browserslist-useragent
è garantire che le stranezze della piattaforma vengano prese in considerazione durante la corrispondenza degli user agent. Ad esempio, tutti i browser su iOS (incluso Chrome) utilizzano WebKit come motore sottostante e verranno abbinati alla rispettiva query Browserslist specifica per Safari.
Potrebbe non essere prudente fare affidamento esclusivamente sulla correttezza dell'analisi dell'agente utente in produzione. Ricorrendo al pacchetto legacy per i browser che non sono definiti nell'elenco moderno o che hanno stringhe di user-agent sconosciute o non analizzabili, ci assicuriamo che il nostro sito Web funzioni ancora.
Conclusione: ne vale la pena?
Siamo riusciti a coprire un flusso end-to-end per la spedizione di pacchetti senza rigonfiamenti ai nostri clienti. Ma è ragionevole chiedersi se il sovraccarico di manutenzione che questo aggiunge a un progetto valga i suoi benefici. Valutiamo i pro e i contro di questo approccio:
1. Manutenzione e collaudo
Uno è necessario per mantenere solo una singola configurazione Browserslist che alimenta tutti gli strumenti in questa pipeline. L'aggiornamento delle definizioni dei browser moderni e legacy può essere eseguito in qualsiasi momento in futuro senza dover eseguire il refactoring di configurazioni o codice di supporto. Direi che questo rende il sovraccarico di manutenzione quasi trascurabile.
Esiste, tuttavia, un piccolo rischio teorico associato all'affidarsi a Babel per produrre due diversi bundle di codice, ognuno dei quali deve funzionare correttamente nel rispettivo ambiente.
Sebbene gli errori dovuti alle differenze nei bundle possano essere rari, il monitoraggio di queste varianti per gli errori dovrebbe aiutare a identificare e mitigare efficacemente eventuali problemi.
2. Tempo di costruzione e tempo di esecuzione
A differenza di altre tecniche oggi diffuse, tutte queste ottimizzazioni si verificano in fase di compilazione e sono invisibili al cliente.
3. Velocità progressivamente migliorata
L'esperienza degli utenti sui browser moderni diventa significativamente più veloce, mentre gli utenti sui browser legacy continuano a ricevere lo stesso pacchetto di prima, senza conseguenze negative.
4. Utilizzo semplice delle moderne funzionalità del browser
Spesso evitiamo di utilizzare le nuove funzionalità del browser a causa delle dimensioni dei polyfill necessari per utilizzarle. A volte, scegliamo anche polyfill più piccoli non conformi alle specifiche per risparmiare sulle dimensioni. Questo nuovo approccio ci consente di utilizzare polyfill conformi alle specifiche senza preoccuparci di influenzare tutti gli utenti.
Pacchetto differenziale in servizio in produzione
Dati i vantaggi significativi, abbiamo adottato questa pipeline di costruzione durante la creazione di una nuova esperienza di cassa mobile per i clienti di Urban Ladder, uno dei più grandi rivenditori di mobili e decorazioni dell'India.
Nel nostro pacchetto già ottimizzato, siamo stati in grado di ottenere risparmi di circa il 20% sulle risorse CSS e JavaScript di Gzip inviate via cavo ai moderni utenti mobili. Poiché oltre l'80% dei nostri visitatori giornalieri utilizzava questi browser sempreverdi, lo sforzo profuso è valso l'impatto.
Ulteriori risorse
- "Caricare Polyfill solo quando necessario", Philip Walton
-
@babel/preset-env
Un preset di Babele intelligente - Elenco browser "Strumenti"
Ecosistema di plugin creato per Browserslist - Posso usare
Tabella delle quote di mercato del browser attuale