Pubblica, distribuisci e installa il moderno codice JavaScript per applicazioni più veloci

Migliora le prestazioni attivando l'output e le dipendenze JavaScript moderne.

Oltre il 90% dei browser è in grado di eseguire JavaScript moderno, ma la prevalenza della versione precedente di JavaScript rimane oggi una fonte importante di problemi di prestazioni sul web.

JavaScript moderno

Il codice JavaScript moderno non è caratterizzato da codice scritto in una specifica versione specifica di ECMAScript, ma piuttosto da una sintassi supportata da tutti i browser moderni. I browser web moderni come Chrome, Edge, Firefox e Safari costituiscono oltre il 90% del mercato dei browser, mentre i diversi browser che si basano sugli stessi motori di rendering sottostanti 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:

  • Corsi (ES2015)
  • Funzioni a freccia (ES2015)
  • generatori (ES2015)
  • Determinazione dell'ambito dei blocchi (ES2015)
  • Distruzione (ES2015)
  • Parametri di riposo e di diffusione (ES2015)
  • Breve descrizione dell'oggetto (ES2015)
  • Async/await (ES2017)

Le funzionalità nelle versioni più recenti della specifica della lingua 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, che corrisponde ancora alla maggior parte dei browser, ma non a sufficienza che sia sicuro affidarsi a queste funzionalità direttamente. Ciò significa che, sebbene JavaScript "moderno" sia un bersaglio mobile, ES2017 offre la più ampia gamma di compatibilità dei browser e include la maggior parte delle funzionalità di sintassi moderne comunemente utilizzate. In altre parole, ES2017 è la sintassi più vicina alla sintassi moderna oggi.

JavaScript precedente

JavaScript legacy è codice che evita specificamente l'utilizzo di tutte le funzionalità relative al linguaggio sopra riportate. La maggior parte degli sviluppatori scrive il codice sorgente usando una sintassi moderna, ma compila tutto con la sintassi legacy per un maggiore supporto del browser. La compilazione di una sintassi legacy aumenta il supporto del browser, ma l'effetto è spesso minore di quanto ci rendiamo conto. In molti casi, l'assistenza aumenta da circa il 95% al 98%, comportando un costo significativo:

  • JavaScript legacy è in genere più grande del 20% e più lento rispetto al codice moderno equivalente. Le carenze di strumenti e gli errori di configurazione spesso aumentano ulteriormente.

  • Le librerie installate rappresentano fino al 90% del tipico codice JavaScript di produzione. Il codice libreria comporta un overhead JavaScript legacy ancora più elevato a causa del polyfill e della duplicazione dell'helper, che potrebbe essere evitato pubblicando codice moderno.

JavaScript moderno su npm

Recentemente, 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" implicano una versione del nodo pari ad almeno 12.8, che supporta ES2019. Ciò significa che qualsiasi modulo a cui viene fatto riferimento utilizzando il campo "exports" può essere scritto nel codice JavaScript moderno. I consumer dei pacchetti devono presupporre che i moduli con un campo "exports" contengano codice moderno ed eseguire il transpile, se necessario.

Solo moderno

Se vuoi pubblicare un pacchetto con codice moderno e lasciare che sia il consumatore a occuparsi della gestione del transpile quando lo utilizza come dipendenza, utilizza solo il campo "exports".

{
  "name": "foo",
  "exports": "./modern.js"
}

Moderna con fallback precedente

Utilizza il campo "exports" insieme a "main" per pubblicare il pacchetto utilizzando il codice moderno, ma includi anche un elemento di riserva ES5 + CommonJS per i browser legacy.

{
  "name": "foo",
  "exports": "./modern.js",
  "main": "./legacy.cjs"
}

Moderna con ottimizzazioni dei bundler di fallback ed ESM precedenti

Oltre a definire un punto di ingresso CommonJS di riserva, il campo "module" può essere utilizzato per indirizzare a un bundle di riserva precedente simile, ma che utilizza la sintassi dei moduli JavaScript (import e export).

{
  "name": "foo",
  "exports": "./modern.js",
  "main": "./legacy.cjs",
  "module": "./module.js"
}

Molti bundler, come webpack e Rollup, si basano su questo campo per sfruttare le funzionalità dei moduli e abilitare l'scuotimento degli alberi. Si tratta ancora di un bundle legacy che non contiene codice moderno oltre alla sintassi import/export, quindi utilizza questo approccio per fornire codice moderno con un fallback legacy ancora ottimizzato per il raggruppamento.

JavaScript moderno nelle applicazioni

Le dipendenze di terze parti costituiscono la stragrande maggioranza del codice JavaScript di produzione tipico nelle applicazioni web. Sebbene le dipendenze npm siano state storicamente pubblicate come sintassi ES5 legacy, questa non è più un'ipotesi sicura e rischia che gli aggiornamenti delle dipendenze possano interrompere il supporto del browser nell'applicazione.

Con un numero crescente di pacchetti npm che passano al moderno JavaScript, è importante assicurarsi che gli strumenti di creazione siano configurati per gestirli. C'è una buona probabilità che alcuni dei pacchetti npm che usi utilizzino già funzionalità moderne. Sono disponibili diverse opzioni per utilizzare il codice moderno da npm senza interrompere l'applicazione nei browser meno recenti, ma l'idea generale è fare in modo che il sistema di build transpili le dipendenze allo stesso target di sintassi del codice sorgente.

webpack

A partire da Webpack 5, è ora possibile configurare la sintassi che verrà utilizzata da Webpack durante la generazione del codice per bundle e moduli. Ciò non esegue il transpile del codice o delle dipendenze, ma influisce solo sul codice "colla" generato dal webpack. Per specificare il target del supporto del browser, aggiungi una configurazione del browserslist al progetto o esegui questa operazione direttamente nella configurazione del webpack:

module.exports = {
  target: ['web', 'es2017'],
};

È anche possibile configurare il webpack per generare bundle ottimizzati che omettono funzioni wrapper non necessarie quando scegli come target un ambiente moderno ES Modules. Inoltre, il webpack viene configurato in modo da caricare bundle basati sul codice suddiviso utilizzando <script type="module">.

module.exports = {
  target: ['web', 'es2017'],
  output: {
    module: true,
  },
  experiments: {
    outputModule: true,
  },
};

Sono disponibili numerosi plug-in webpack che permettono di compilare e distribuire il codice JavaScript moderno continuando comunque a supportare i browser precedenti, come il plug-in Optimize e BabelEsmPlugin.

Plug-in Optimize

Il plug-in di Optimize è un plug-in webpack che trasforma il codice finale in bundle da JavaScript moderno a quello precedente, invece di ogni singolo file sorgente. Si tratta di una configurazione autonoma che consente alla configurazione del webpack di presumere che sia tutto un codice JavaScript moderno senza diramazioni speciali per più output o sintassi.

Poiché il plug-in Optimize opera su bundle anziché su singoli moduli, elabora il codice della tua applicazione e le tue dipendenze in modo uguale. Ciò rende sicuro l'utilizzo delle dipendenze JavaScript moderne da npm, poiché il loro codice verrà raggruppato ed eseguito il transpile nella sintassi corretta. Può anche essere più veloce delle soluzioni tradizionali che prevedono due passaggi di compilazione, generando comunque bundle separati per i browser moderni e legacy. I due set di bundle sono progettati per essere caricati utilizzando il pattern/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 codice moderno e legacy separatamente. Gestisce anche l'esecuzione di Babel e minimizza i bundle utilizzando Terser con impostazioni ottimali separate per gli output moderni e legacy. 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.

Confronto: il transpiling dei moduli di origine viene eseguito due volte rispetto ai bundle generati con il transpiling.

BabelEsmPlugin

BabelEsmPlugin è un plug-in per il webpack che funziona insieme a @babel/preset-env per generare versioni moderne di bundle esistenti per inviare codice meno transpilato ai browser moderni. È la soluzione pronta all'uso più popolare permodule/nomodule, utilizzata da Next.js e dall'interfaccia a riga di comando Preact.

// 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, dato che esegue due build dell'applicazione prevalentemente separate. La compilazione due volte può richiedere qualche tempo in più per le applicazioni di grandi dimensioni, tuttavia questa tecnica consente a BabelEsmPlugin di integrarsi perfettamente nelle configurazioni webpack esistenti e lo rende una delle opzioni più pratiche 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 moderni moduli npm di JavaScript. La definizione di due configurazioni babel-loader distinte consente di compilare automaticamente le funzionalità di linguaggio moderne presenti in node_modules in ES2017, eseguendo comunque il transpiling del tuo codice proprietario con i plug-in e le preimpostazioni Babel definiti nella configurazione del tuo progetto. Questa operazione non genera bundle moderni e legacy per una configurazione di moduli/nomodule, ma consente di installare e utilizzare pacchetti npm che contengono codice JavaScript moderno senza interrompere i browser meno recenti.

webpack-plugin-modern-npm utilizza questa tecnica per compilare dipendenze npm che hanno un campo "exports" nel file package.json, poiché potrebbero 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 manualmente la tecnica nella configurazione del webpack controllando la presenza di un campo "exports" in package.json dei moduli man mano che vengono risolti. Omettendo la memorizzazione nella cache per brevità, un'implementazione personalizzata potrebbe essere la seguente:

// 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'],
          },
        },
      },
    ],
  },
};

Se utilizzi questo approccio, dovrai assicurarti che la sintassi moderna sia supportata dal minificatore. Sia Terser che uglify-es hanno un'opzione per specificare {ecma: 2017} al fine di mantenere e, in alcuni casi, generare la sintassi ES2017 durante la compressione e la formattazione.

Riepilogo

Rollup offre supporto integrato per la generazione di più set di bundle come parte di un'unica build e genera codice moderno per impostazione predefinita. Di conseguenza, Rollup può essere configurato per generare bundle 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 in bundle generati anziché in singoli moduli di origine. Rollup offre supporto integrato per la generazione di più set di bundle come parte di un'unica build, ciascuno con i propri plug-in. Puoi utilizzarlo per produrre diversi bundle per modelli moderni e legacy passando ciascuno a una diversa configurazione del plug-in di output 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 creazione aggiuntivi

Le proprietà di aggregazione e webpack sono altamente configurabili, il che in genere significa che ogni progetto deve aggiornare la propria configurazione e attivare la sintassi JavaScript moderna nelle dipendenze. Esistono anche strumenti di creazione di livello superiore che preferiscono le convenzioni e i valori predefiniti rispetto alla configurazione, come Parcel, Snowpack, Vite e WMR. La maggior parte di questi strumenti presuppone che le dipendenze npm possano contenere una sintassi moderna e le trasferisci ai livelli di sintassi appropriati durante la creazione per la produzione.

Oltre ai plug-in dedicati per webpack e Rollup, è possibile aggiungere a qualsiasi progetto utilizzando devolution i moderni bundle JavaScript con fallback legacy. Devolution è uno strumento autonomo che trasforma l'output di un sistema di compilazione per produrre varianti JavaScript legacy, consentendo il raggruppamento e le trasformazioni di assumere un target di output moderno.