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

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 precedente rimane una grande fonte di problemi di prestazioni sul web oggi.

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)
  • Async/await (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 e ES2021 sono supportate solo nel 70% del mercato dei browser, ovvero nella maggior parte dei browser, ma non in misura sufficiente per poter fare affidamento direttamente su queste 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 in 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 lento rispetto al codice moderno equivalente. Spesso, le carenze degli strumenti e gli errori di configurazione contribuiscono ad aumentare 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 i punti di contatto di 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 assumere che i moduli con un campo "exports" contengano codice moderno e eseguire la transpilazione, 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 basano su questo campo per sfruttare le funzionalità dei moduli e attivare il tree shaking. 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 gli aggiornamenti delle dipendenze rischiano di interrompere il supporto del browser nella tua applicazione.

Con un numero crescente di pacchetti npm che passano a JavaScript moderno, è importante assicurarsi che gli strumenti di compilazione 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. Il codice o le dipendenze non vengono transpilati, ma viene modificato solo il codice "glue" generato da webpack. Per specificare il target di supporto del browser, aggiungi una configurazione di browserslist al tuo progetto o esegui l'operazione direttamente nella configurazione di 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, configura webpack in modo da caricare i bundle con suddivisione del 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. Si tratta di una configurazione autosufficiente che consente alla configurazione webpack di assumere che tutto sia JavaScript moderno senza ramificazioni speciali per più output o sintassi.

Poiché il plug-in di ottimizzazione opera su bundle anziché su singoli moduli, elabora in modo uguale il codice dell'applicazione e le dipendenze. In questo modo, è possibile utilizzare in sicurezza le dipendenze JavaScript moderne di npm, perché il loro codice verrà aggregato e transpiled nella 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 per i bundle legacy generati vengono estratti in uno script dedicato in modo che non vengano mai duplicati o caricati inutilmente nei browser più recenti.

Confronto: transpilazione dei moduli di origine due volte rispetto alla transpilazione dei bundle generati.

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 della tua applicazione in gran parte separate. 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 transpilare 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 di 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 le dipendenze npm che hanno un campo "exports" nel loro package.json, poiché potrebbero contenere 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 di webpack controllando la presenza di 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, ciascuno con i propri plug-in. Puoi utilizzarlo per produrre diversi bundle per le versioni moderne e precedenti passando ciascuno attraverso una configurazione diversa 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.