Pubblica codice moderno per i browser moderni per caricare più velocemente le pagine

In questo codelab, migliora le prestazioni di questa semplice applicazione che consente agli utenti di valutare gatti casuali. Scopri come ottimizzare il bundle JavaScript riducendo al minimo la quantità di codice transpirata.

Screenshot dell'app

Nell'app di esempio, puoi selezionare una parola o un'emoji per comunicare quanto ti piace ogni gatto. Quando fai clic su un pulsante, l'app visualizza il valore del pulsante sotto l'immagine del gatto corrente.

Misurazione

È sempre opportuno iniziare a esaminare un sito web prima di aggiungere eventuali ottimizzazioni:

  1. Per visualizzare l'anteprima del sito, premi Visualizza app, quindi Schermo intero schermo intero.
  2. Premi "Ctrl+Maiusc+J" (o "Comando+Opzione+J" su Mac) per aprire DevTools.
  3. Fai clic sulla scheda Rete.
  4. Seleziona la casella di controllo Disabilita cache.
  5. Ricarica l'app.

Richiesta originale per le dimensioni del bundle

Per questa applicazione vengono utilizzati più di 80 kB. È il momento di scoprire se alcune parti del bundle non vengono utilizzate:

  1. Premi Control+Shift+P (o Command+Shift+P su Mac) per aprire il menu Comando. Menu Comando

  2. Inserisci Show Coverage e premi Enter per visualizzare la scheda Copertura.

  3. Nella scheda Copertura, fai clic su Ricarica per ricaricare l'applicazione durante l'acquisizione della copertura.

    Ricarica l'app con copertura del codice

  4. Dai un'occhiata a quanto codice è stato utilizzato rispetto a quanto è stato caricato per il bundle principale:

    Copertura del codice del bundle

Oltre la metà del bundle (44 kB) non viene nemmeno utilizzata. Questo perché gran parte del codice al suo interno è costituito da polyfill per garantire che l'applicazione funzioni nei browser meno recenti.

Utilizza @babel/preset-env

La sintassi del linguaggio JavaScript è conforme a uno standard noto come ECMAScript o ECMA-262. Le versioni più recenti della specifica vengono rilasciate ogni anno e includono nuove funzionalità che hanno superato il processo di proposta. Ognuno dei browser principali si trova sempre in una fase diversa di supporto di queste funzionalità.

Nell'applicazione vengono utilizzate le seguenti funzionalità di ES2015:

Viene utilizzata anche la seguente funzionalità di ES2017:

Immergiti nel codice sorgente in src/index.js per vedere come viene utilizzato tutto questo.

Tutte queste funzionalità sono supportate nell'ultima versione di Chrome, ma cosa succede con gli altri browser che non le supportano? Babel, inclusa nell'applicazione, è la libreria più utilizzata per compilare il codice contenente una sintassi più recente nel codice che i browser e gli ambienti meno recenti sono in grado di comprendere. Ci sono due modi per farlo:

  • I Polyfill sono inclusi per emulare le più recenti funzioni di ES2015+ in modo che le relative API possano essere utilizzate anche se non sono supportate dal browser. Ecco un esempio di un polyfill del metodo Array.includes.
  • I plug-in vengono utilizzati per trasformare il codice ES2015 (o versioni successive) nella sintassi ES5 precedente. Poiché si tratta di modifiche legate alla sintassi (come le funzioni a freccia), non possono essere emulate con i polyfill.

Controlla package.json per vedere quali librerie Babel sono incluse:

"dependencies": {
  "@babel/polyfill": "^7.0.0"
},
"devDependencies": {
  //...
  "babel-loader": "^8.0.2",
  "@babel/core": "^7.1.0",
  "@babel/preset-env": "^7.1.0",
  //...
}
  • @babel/core è il compilatore principale di Babel. In questo modo, tutte le configurazioni di Babel sono definite in un elemento .babelrc alla base del progetto.
  • babel-loader include Babel nel processo di compilazione del webpack.

Ora dai un'occhiata a webpack.config.js per vedere come viene inclusa una regola babel-loader:

module: {
  rules: [
    //...
    {
      test: /\.js$/,
      exclude: /node_modules/,
      loader: "babel-loader"
    }
  ]
},
  • @babel/polyfill fornisce tutti i polyfill necessari per tutte le funzionalità ECMAScript più recenti in modo da poter lavorare in ambienti che non li supportano. È già stata importata nella parte superiore di src/index.js.
import "./style.css";
import "@babel/polyfill";
  • @babel/preset-env identifica le trasformazioni e i polyfill necessari per i browser o gli ambienti scelti come target.

Dai un'occhiata al file di configurazione di Babel, .babelrc, per vedere come è incluso:

{
  "presets": [
    [
      "@babel/preset-env",
      {
        "targets": "last 2 versions"
      }
    ]
  ]
}

Questa è una configurazione Babel e Webpack. Scopri come includere Babel nella tua applicazione se utilizzi un bundler del modulo diverso da webpack.

L'attributo targets in .babelrc identifica i browser presi di mira. @babel/preset-env si integra con l'elenco dei browser, il che significa che puoi trovare un elenco completo delle query compatibili che possono essere utilizzate in questo campo nella documentazione dei browser.

Il valore "last 2 versions" esegue il transpile del codice nell'applicazione per le ultime due versioni di ogni browser.

Debug

Per una panoramica completa di tutti i target Babel del browser nonché di tutte le trasformazioni e polyfill inclusi, aggiungi un campo debug a .babelrc:

{
  "presets": [
    [
      "@babel/preset-env",
      {
        "targets": "last 2 versions",
        "debug": true
      }
    ]
  ]
}
  • Fai clic su Strumenti.
  • Fai clic su Log.

Ricarica l'applicazione e dai un'occhiata ai log di stato di Glitch nella parte inferiore dell'editor.

Browser scelti come target

Babel registra nella console una serie di dettagli sul processo di compilazione, inclusi tutti gli ambienti di destinazione per cui il codice è stato compilato.

Browser scelti come target

Nota come i browser non più in uso, come Internet Explorer, sono inclusi in questo elenco. Questo è un problema perché ai browser non supportati non verranno aggiunte funzionalità più recenti e Babel continua a trascrivere la sintassi specifica. Questo aumenta inutilmente le dimensioni del bundle se gli utenti non utilizzano questo browser per accedere al tuo sito.

Babel registra inoltre un elenco di plug-in di trasformazione utilizzati:

Elenco dei plug-in utilizzati

È un elenco piuttosto lungo. Questi sono tutti i plug-in che Babel deve utilizzare per trasformare qualsiasi sintassi di ES2015+ in una sintassi precedente per tutti i browser di destinazione.

Tuttavia, Babel non mostra i polyfill specifici utilizzati:

Nessun polyfill aggiunto

Questo perché l'intero @babel/polyfill viene importato direttamente.

Carica i polyfill singolarmente

Per impostazione predefinita, Babel include ogni polyfill necessario per un ambiente ES2015+ completo quando @babel/polyfill viene importato in un file. Per importare polyfill specifici necessari per i browser di destinazione, aggiungi un useBuiltIns: 'entry' alla configurazione.

{
  "presets": [
    [
      "@babel/preset-env",
      {
        "targets": "last 2 versions",
        "debug": true
        "useBuiltIns": "entry"
      }
    ]
  ]
}

Ricarica l'applicazione. Ora puoi vedere tutti i polyfill specifici inclusi:

Elenco di polyfill importati

Ora sono inclusi solo i polyfill necessari per "last 2 versions", ma si tratta comunque di un elenco molto lungo. Questo perché è ancora incluso il polyfill necessario per i browser di destinazione per ogni funzionalità più recente. Modifica il valore dell'attributo in usage per includere solo quelle necessarie per le caratteristiche utilizzate nel codice.

{
  "presets": [
    [
      "@babel/preset-env",
      {
        "targets": "last 2 versions",
        "debug": true,
        "useBuiltIns": "entry"
        "useBuiltIns": "usage"
      }
    ]
  ]
}

In questo modo, i polyfill vengono inclusi automaticamente dove necessario. Questo significa che puoi rimuovere l'importazione @babel/polyfill in src/index.js.

import "./style.css";
import "@babel/polyfill";

Ora sono inclusi solo i polyfill richiesti necessari per l'applicazione.

Elenco di polyfill inclusi automaticamente

Le dimensioni del bundle di applicazioni sono ridotte in modo significativo.

Dimensioni del bundle ridotte a 30,1 kB

Limitazione dell'elenco dei browser supportati

Il numero di destinazioni dei browser inclusi è ancora piuttosto elevato e non molti utenti utilizzano browser non più in uso, come Internet Explorer. Aggiorna le configurazioni come segue:

{
  "presets": [
    [
      "@babel/preset-env",
      {
        "targets": "last 2 versions",
        "targets": [">0.25%", "not ie 11"],
        "debug": true,
        "useBuiltIns": "usage",
      }
    ]
  ]
}

Dai un'occhiata ai dettagli del bundle recuperato.

Dimensione del pacchetto da 30,0 kB

Poiché l'applicazione è così piccola, non c'è molta differenza con queste modifiche. Tuttavia, l'utilizzo di una percentuale di quota di mercato dei browser (ad esempio ">0.25%") ed esclusione di browser specifici che ritieni sicuri che gli utenti non utilizzino è l'approccio consigliato. Per saperne di più, dai un'occhiata all'articolo di James Kyle "Ultime 2 versioni" considerate dannose.

Usa <script type="module">

C'è ancora margine di miglioramento. Anche se sono stati rimossi alcuni polyfill non utilizzati, molti di questi vengono spediti che non sono necessari per alcuni browser. Grazie ai moduli, è possibile scrivere e inviare direttamente ai browser sintesi più recenti senza l'utilizzo di polyfill non necessari.

I moduli JavaScript sono una funzionalità relativamente nuova supportata in tutti i principali browser. I moduli possono essere creati utilizzando un attributo type="module" per definire script che eseguono l'importazione e l'esportazione da altri moduli. Ad esempio:

// math.mjs
export const add = (x, y) => x + y;

<!-- index.html -->
<script type="module">
  import { add } from './math.mjs';

  add(5, 2); // 7
</script>

Molte funzionalità ECMAScript più recenti sono già supportate in ambienti che supportano i moduli JavaScript (invece di aver bisogno di Babel). Ciò significa che la configurazione di Babel può essere modificata per inviare due diverse versioni dell'applicazione al browser:

  • Una versione che funzionerà nei browser più recenti che supportano i moduli e che include un modulo in gran parte senza transpile, ma con dimensioni del file inferiori
  • Una versione che include uno script più grande e con elementi transcompilati che funzionerebbe in qualsiasi browser precedente

Utilizzo dei moduli ES con Babel

Per avere impostazioni @babel/preset-env separate per le due versioni dell'applicazione, rimuovi il file .babelrc. Le impostazioni di Babel possono essere aggiunte alla configurazione webpack specificando due diversi formati di compilazione per ogni versione dell'applicazione.

Inizia aggiungendo una configurazione per lo script precedente a webpack.config.js:

const legacyConfig = {
  entry,
  output: {
    path: path.resolve(__dirname, "public"),
    filename: "[name].bundle.js"
  },
  module: {
    rules: [
      {
        test: /\.js$/,
        exclude: /node_modules/,
        loader: "babel-loader",
        options: {
          presets: [
            ["@babel/preset-env", {
              useBuiltIns: "usage",
              targets: {
                esmodules: false
              }
            }]
          ]
        }
      },
      cssRule
    ]
  },
  plugins
}

Nota che invece di utilizzare il valore targets per "@babel/preset-env", viene utilizzato esmodules con il valore false. Ciò significa che Babel include tutte le trasformazioni e i polyfill necessari per scegliere come target tutti i browser che non supportano ancora i moduli ES.

Aggiungi oggetti entry, cssRule e corePlugins all'inizio del file webpack.config.js. Tutti questi elementi sono condivisi tra gli script del modulo e legacy pubblicati nel browser.

const entry = {
  main: "./src"
};

const cssRule = {
  test: /\.css$/,
  use: ExtractTextPlugin.extract({
    fallback: "style-loader",
    use: "css-loader"
  })
};

const plugins = [
  new ExtractTextPlugin({filename: "[name].css", allChunks: true}),
  new HtmlWebpackPlugin({template: "./src/index.html"})
];

Allo stesso modo, crea un oggetto di configurazione per lo script del modulo riportato di seguito, dove è definito legacyConfig:

const moduleConfig = {
  entry,
  output: {
    path: path.resolve(__dirname, "public"),
    filename: "[name].mjs"
  },
  module: {
    rules: [
      {
        test: /\.js$/,
        exclude: /node_modules/,
        loader: "babel-loader",
        options: {
          presets: [
            ["@babel/preset-env", {
              useBuiltIns: "usage",
              targets: {
                esmodules: true
              }
            }]
          ]
        }
      },
      cssRule
    ]
  },
  plugins
}

La differenza principale qui è che per il nome file di output viene utilizzata un'estensione del file .mjs. Il valore esmodules è impostato su true qui, il che significa che il codice generato in questo modulo è uno script più piccolo e meno compilato che non esegue alcuna trasformazione in questo esempio, poiché tutte le funzionalità utilizzate sono già supportate nei browser che supportano i moduli.

Alla fine del file, esporta entrambe le configurazioni in un unico array.

module.exports = [
  legacyConfig, moduleConfig
];

Ora in questo modo viene creato un modulo più piccolo per i browser che lo supportano e uno script più grande per i browser meno recenti.

I browser che supportano i moduli ignorano gli script con un attributo nomodule. Al contrario, i browser che non supportano i moduli ignorano gli elementi di script con type="module". Ciò significa che puoi includere un modulo e un video di riserva compilato. Idealmente, le due versioni dell'applicazione dovrebbero essere in index.html in questo modo:

<script type="module" src="main.mjs"></script>
<script nomodule src="main.bundle.js"></script>

I browser che supportano i moduli recuperano ed eseguono main.mjs e ignorano main.bundle.js. I browser che non supportano i moduli fanno il contrario.

È importante notare che, a differenza dei normali script, quelli dei moduli vengono sempre differiti per impostazione predefinita. Se vuoi che anche lo script nomodule equivalente venga differito ed eseguito solo dopo l'analisi, devi aggiungere l'attributo defer:

<script type="module" src="main.mjs"></script>
<script nomodule src="main.bundle.js" defer></script>

L'ultima cosa da fare qui è aggiungere gli attributi module e nomodule rispettivamente al modulo e allo script precedente, importando ScriptExtHtmlWebpackPlugin all'inizio di webpack.config.js:

const path = require("path");

const webpack = require("webpack");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const ScriptExtHtmlWebpackPlugin = require("script-ext-html-webpack-plugin");

Ora aggiorna l'array plugins nelle configurazioni per includere questo plug-in:

const plugins = [
  new ExtractTextPlugin({filename: "[name].css", allChunks: true}),
  new HtmlWebpackPlugin({template: "./src/index.html"}),
  new ScriptExtHtmlWebpackPlugin({
    module: /\.mjs$/,
    custom: [
      {
        test: /\.js$/,
        attribute: 'nomodule',
        value: ''
    },
    ]
  })
];

Queste impostazioni del plug-in aggiungono un attributo type="module" per tutti gli elementi script .mjs e un attributo nomodule per tutti i moduli script .js.

Moduli di pubblicazione nel documento HTML

L'ultima cosa da fare è inviare sia gli elementi di script legacy che quelli moderni nel file HTML. Purtroppo il plug-in che crea il file HTML finale, HTMLWebpackPlugin, al momento non supporta l'output degli script modulo e nomodule. Sebbene esistano soluzioni alternative e plug-in separati creati per risolvere questo problema, ad esempio BabelMultiTargetPlugin e HTMLWebpackMultiBuildPlugin, ai fini di questo tutorial viene utilizzato un approccio più semplice per aggiungere manualmente l'elemento di script del modulo.

Aggiungi il seguente codice a src/index.js alla fine del file:

    ...
    </form>
    <script type="module" src="main.mjs"></script>
  </body>
</html>

Ora carica l'applicazione in un browser che supporta i moduli, ad esempio l'ultima versione di Chrome.

Modulo da 5,2 kB recuperato sulla rete per i browser più recenti

Viene recuperato solo il modulo, con una dimensione del bundle molto inferiore perché è in gran parte non trapanata. L'altro elemento script viene completamente ignorato dal browser.

Se carichi l'applicazione su un browser meno recente, viene recuperato solo lo script più grande e trapelato con tutti i polyfill e le trasformazioni necessari. Ecco uno screenshot di tutte le richieste effettuate su una versione precedente di Chrome (versione 38).

Script recuperato da 30 kB per browser meno recenti

Conclusione

Ora sai come utilizzare @babel/preset-env per fornire solo i polyfill necessari necessari per i browser scelti come target. Sai anche come i moduli JavaScript possono migliorare ulteriormente le prestazioni inviando due diverse versioni con transpile di un'applicazione. Con una buona comprensione di come entrambe queste tecniche possono ridurre in modo significativo le dimensioni dei pacchetti, vai avanti e ottimizza.