Migliora le prestazioni attivando le dipendenze e l'output di JavaScript moderni.
Oltre il 90% dei browser è in grado di eseguire JavaScript moderno, ma la prevalenza di JavaScript legacy rimane oggi una grande fonte di problemi di prestazioni sul web.
JavaScript moderno
Il codice JavaScript moderno non è caratterizzato come codice scritto in una versione specifica della specifica ECMAScript, ma piuttosto in una sintassi supportata da tutti i browser moderni. I browser web moderni come Chrome, Edge, Firefox e Safari rappresentano più del 90% del mercato dei browser e altri browser che si basano sugli stessi motori di rendering di base rappresentano un ulteriore 5%. Ciò significa che il 95% del traffico web globale proviene da browser che supportano le funzionalità del linguaggio JavaScript più utilizzate negli ultimi 10 anni, tra cui:
- Classi (ES2015)
- Funzioni freccia (ES2015)
- Generatori (ES2015)
- Ambito del blocco (ES2015)
- Distruzione (ES2015)
- Parametri rest e spread (ES2015)
- Abbreviazione di oggetti (ES2015)
- Asinc/attesa (ES2017)
Le funzionalità delle versioni più recenti della specifica del linguaggio in genere hanno un supporto meno coerente nei browser moderni. Ad esempio, molte funzionalità di ES2020 ed ES2021 sono supportate solo nel 70% del mercato dei browser, pur rimanendo la maggior parte dei browser, ma non abbastanza da poter fare affidamento direttamente su quelle funzionalità. Ciò significa che, anche se JavaScript "moderno" è un target in movimento, ES2017 offre la gamma più ampia di compatibilità con i browser, includendo al contempo la maggior parte delle funzionalità di sintassi moderna di uso comune. In altre parole, ES2017 è la versione più vicina alla sintassi moderna.
JavaScript precedente
Il codice JavaScript precedente evita specificamente di utilizzare tutte le funzionalità del linguaggio sopra indicate. La maggior parte degli sviluppatori scrive il codice sorgente utilizzando una sintassi moderna, ma compila tutto con la sintassi precedente per un maggiore supporto del browser. La compilazione con la sintassi precedente aumenta il supporto dei browser, ma l'effetto è spesso minore di quanto si pensi. In molti casi, il supporto aumenta da circa il 95% al 98% con un costo significativo:
Il codice JavaScript precedente è in genere circa il 20% più grande e più lento rispetto al codice moderno equivalente. Le carenze degli strumenti e gli errori di configurazione spesso ampliano ulteriormente questo divario.
Le librerie installate rappresentano fino al 90% del codice JavaScript di produzione tipico. Il codice della libreria comporta un overhead di JavaScript legacy ancora maggiore a causa della duplicazione di polyfill e helper che potrebbe essere evitata pubblicando codice moderno.
JavaScript moderno su npm
Di recente, Node.js ha standardizzato un campo "exports"
per definire
punti di ingresso per un pacchetto:
{
"exports": "./index.js"
}
I moduli a cui fa riferimento il campo "exports"
presuppongono una versione di Node di almeno 12.8, che supporta ES2019. Ciò significa che qualsiasi modulo a cui viene fatto riferimento utilizzando il campo "exports"
può essere scritto in JavaScript moderno. I consumatori del pacchetto devono presupporre che i moduli con un campo "exports"
contengano codice moderno e che vengano trasferiti se necessario.
Solo moderno
Se vuoi pubblicare un pacchetto con codice moderno e lasciare al consumatore il compito di gestire la transpilazione quando lo utilizza come dipendenza, utilizza solo il campo "exports"
.
{
"name": "foo",
"exports": "./modern.js"
}
Moderno con fallback legacy
Utilizza il campo "exports"
insieme a "main"
per pubblicare il tuo pacchetto utilizzando codice moderno, ma includi anche un fallback ES5 + CommonJS per i browser legacy.
{
"name": "foo",
"exports": "./modern.js",
"main": "./legacy.cjs"
}
Moderno con fallback legacy e ottimizzazioni del bundler ESM
Oltre a definire un punto di contatto CommonJS di riserva, il campo "module"
può essere utilizzato per fare riferimento a un bundle di riserva precedente simile, ma che utilizza la sintassi del modulo JavaScript (import
e export
).
{
"name": "foo",
"exports": "./modern.js",
"main": "./legacy.cjs",
"module": "./module.js"
}
Molti bundler, come webpack e Rollup, si affidano a questo campo per sfruttare le funzionalità dei moduli e consentire l'effetto "treeshaking".
Si tratta ancora di un bundle precedente che non contiene codice moderno, a parte la sintassi import
/export
, quindi utilizza questo approccio per pubblicare codice moderno con un fallback precedente ancora ottimizzato per il bundling.
JavaScript moderno nelle applicazioni
Le dipendenze di terze parti costituiscono la stragrande maggioranza del codice JavaScript di produzione tipico nelle applicazioni web. Sebbene in passato le dipendenze npm siano state pubblicate come sintassi ES5 precedente, questa non è più un'ipotesi sicura e rischia di causare l'interruzione del supporto del browser nella tua applicazione a causa degli aggiornamenti delle dipendenze.
Con un numero crescente di pacchetti npm che passano al codice JavaScript moderno, è importante garantire che gli strumenti di creazione siano configurati per gestirli. È molto probabile che alcuni dei pacchetti npm di cui hai bisogno utilizzino già funzionalità linguistiche moderne. Esistono diverse opzioni per utilizzare il codice moderno di npm senza interrompere l'applicazione nei browser meno recenti, ma l'idea generale è che il sistema di compilazione traspili le dipendenze nella stessa sintassi di destinazione del codice sorgente.
webpack
A partire da webpack 5, ora è possibile configurare la sintassi che webpack utilizzerà quando genera il codice per i bundle e i moduli. Questa operazione non esegue il transpile del codice o delle dipendenze, ma influisce solo sul codice "glue" generato da webpack. Per specificare il target del supporto dei browser, aggiungi una configurazione dell'elenco dei browser al tuo progetto, oppure eseguila direttamente nella configurazione del webpack:
module.exports = {
target: ['web', 'es2017'],
};
È anche possibile configurare webpack in modo da generare bundle ottimizzati che omettono le funzioni di wrapper non necessarie quando si ha come target un ambiente di moduli ES moderno. Inoltre, il webpack viene configurato per caricare pacchetti di suddivisione di codice utilizzando
<script type="module">
.
module.exports = {
target: ['web', 'es2017'],
output: {
module: true,
},
experiments: {
outputModule: true,
},
};
Esistono diversi plug-in webpack che consentono di compilare e pubblicare JavaScript moderno, supportando al contempo i browser precedenti, come Optimize Plugin e BabelEsmPlugin.
Plug-in Optimize
Il plug-in Optimize è un plug-in webpack che trasforma il codice pacchettizzato finale da JavaScript moderno a legacy anziché ogni singolo file sorgente. È una configurazione autonoma che consente alla configurazione webpack di presupporre che tutto sia JavaScript moderno senza diramazioni speciali per più output o sintassi.
Poiché il plug-in di Optimize opera su bundle anziché su singoli moduli, elabora in modo uniforme il codice della tua applicazione e le tue dipendenze. In questo modo è sicuro utilizzare le moderne dipendenze JavaScript di npm, perché il codice verrà bundle e inviato alla sintassi corretta. Può anche essere più veloce rispetto alle soluzioni tradizionali che prevedono due fasi di compilazione, generando comunque bundle separati per i browser moderni e legacy. I due insiemi di bundle sono progettati per essere caricati utilizzando il pattern module/nomodule.
// webpack.config.js
const OptimizePlugin = require('optimize-plugin');
module.exports = {
// ...
plugins: [new OptimizePlugin()],
};
Optimize Plugin
può essere più veloce ed efficiente rispetto alle configurazioni webpack personalizzate, che in genere raggruppano separatamente il codice moderno e legacy. Gestisce inoltre l'esecuzione di Babel e la minimizzazione dei bundle utilizzando Terser con impostazioni ottimali separate per le uscite moderne e precedenti. Infine, i polyfill necessari dai bundle legacy generati vengono estratti in uno script dedicato in modo che non vengano mai duplicati o caricati inutilmente nei browser più recenti.
BabelEsmPlugin
BabelEsmPlugin è un plug-in webpack che funziona con @babel/preset-env per generare versioni moderne dei bundle esistenti al fine di inviare codice meno transpiled ai browser moderni. È la soluzione pronta all'uso più utilizzata per module/nomodule, utilizzata da Next.js e Preact CLI.
// webpack.config.js
const BabelEsmPlugin = require('babel-esm-plugin');
module.exports = {
//...
module: {
rules: [
// your existing babel-loader configuration:
{
test: /\.js$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env'],
},
},
},
],
},
plugins: [new BabelEsmPlugin()],
};
BabelEsmPlugin
supporta un'ampia gamma di configurazioni webpack, perché
esegue due build ampiamente separate dell'applicazione. La compilazione due volte può richiedere un po' di tempo in più per le applicazioni di grandi dimensioni, ma questa tecnica consente di integrare facilmente BabelEsmPlugin
nelle configurazioni webpack esistenti e lo rende una delle opzioni più convenienti disponibili.
Configura Babel-loader per transpile node_modules
Se utilizzi babel-loader
senza uno dei due plug-in precedenti,
è necessario un passaggio importante per utilizzare i moduli npm JavaScript moderni. La definizione di due configurazioni babel-loader
distinte consente di compilare automaticamente le funzionalità linguistiche moderne presenti in node_modules
in ES2017, continuando a transpilare il codice proprietario con i plug-in e i preset Babel definiti nella configurazione del progetto. In questo modo non vengono generati bundle moderni e legacy per una configurazione module/nomodule, ma è possibile installare e utilizzare i pacchetti npm che contengono JavaScript moderno senza interrompere i browser precedenti.
webpack-plugin-modern-npm
utilizza questa tecnica per compilare dipendenze npm con un campo "exports"
nel campo package.json
, poiché possono contenere una sintassi moderna:
// webpack.config.js
const ModernNpmPlugin = require('webpack-plugin-modern-npm');
module.exports = {
plugins: [
// auto-transpile modern stuff found in node_modules
new ModernNpmPlugin(),
],
};
In alternativa, puoi implementare la tecnica manualmente nella configurazione
webpack cercando un campo "exports"
in package.json
dei
moduli man mano che vengono risolti. Se omettiamo la memorizzazione nella cache per brevità, un'implementazione personalizzata potrebbe avere il seguente aspetto:
// webpack.config.js
module.exports = {
module: {
rules: [
// Transpile for your own first-party code:
{
test: /\.js$/i,
loader: 'babel-loader',
exclude: /node_modules/,
},
// Transpile modern dependencies:
{
test: /\.js$/i,
include(file) {
let dir = file.match(/^.*[/\\]node_modules[/\\](@.*?[/\\])?.*?[/\\]/);
try {
return dir && !!require(dir[0] + 'package.json').exports;
} catch (e) {}
},
use: {
loader: 'babel-loader',
options: {
babelrc: false,
configFile: false,
presets: ['@babel/preset-env'],
},
},
},
],
},
};
Quando utilizzi questo approccio, devi assicurarti che la sintassi moderna sia supportata dal tuo compressore. Sia Terser che uglify-es hanno un'opzione per specificare {ecma: 2017}
al fine di preservare e, in alcuni casi, generare la sintassi ES2017 durante la compressione e la formattazione.
Riepilogo
Il raggruppamento ha il supporto integrato per la generazione di più insiemi di bundle nell'ambito di una singola compilazione e genera codice moderno per impostazione predefinita. Di conseguenza, Rollup può essere configurato per generare pacchetti moderni e legacy con i plug-in ufficiali che probabilmente stai già utilizzando.
@rollup/plugin-babel
Se utilizzi Rollup, il
metodo getBabelOutputPlugin()
(fornito dal
plug-in Babel ufficiale di Rollup)
trasforma il codice nei bundle generati anziché nei singoli moduli sorgente.
Rollup supporta la generazione di più insiemi di bundle all'interno di una singola build, ognuno con i propri plug-in. Puoi utilizzarlo per produrre
diversi bundle per moderno e legacy, passando ciascuno attraverso una diversa
configurazione del plug-in di output di Babel:
// rollup.config.js
import {getBabelOutputPlugin} from '@rollup/plugin-babel';
export default {
input: 'src/index.js',
output: [
// modern bundles:
{
format: 'es',
plugins: [
getBabelOutputPlugin({
presets: [
[
'@babel/preset-env',
{
targets: {esmodules: true},
bugfixes: true,
loose: true,
},
],
],
}),
],
},
// legacy (ES5) bundles:
{
format: 'amd',
entryFileNames: '[name].legacy.js',
chunkFileNames: '[name]-[hash].legacy.js',
plugins: [
getBabelOutputPlugin({
presets: ['@babel/preset-env'],
}),
],
},
],
};
Strumenti di compilazione aggiuntivi
Rollup e webpack sono altamente configurabili, il che in genere significa che ogni progetto deve aggiornare la propria configurazione per abilitare la sintassi JavaScript moderna nelle dipendenze. Esistono anche strumenti di compilazione di livello superiore che favoriscono le convenzioni e i valori predefiniti rispetto alla configurazione, come Parcel, Snowpack, Vite e WMR. La maggior parte di questi strumenti assume che le dipendenze npm possano contenere sintassi moderna e le transpilerà al livello o ai livelli di sintassi appropriati durante la compilazione per la produzione.
Oltre ai plug-in dedicati per webpack e Rollup, i bundle JavaScript moderni con fallback precedenti possono essere aggiunti a qualsiasi progetto utilizzando la devoluzione. Devolution è uno strumento autonomo che trasforma l'output di un sistema di compilazione per produrre varianti JavaScript precedenti, consentendo il raggruppamento e le trasformazioni di assumere un target di output moderno.