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 minimizzando la quantità di codice transpilata.

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 mostra il valore del pulsante sotto l'immagine del gatto corrente.

Misura

È sempre buona prassi iniziare a ispezionare un sito web prima di aggiungere ottimizzazioni:

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

Richiesta relativa alle dimensioni del bundle originale

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

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

  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.

    Ricaricare l'app con la copertura del codice

  4. Dai un'occhiata alla quantità di codice utilizzata rispetto a quella caricata 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 il funzionamento dell'applicazione nei browser meno recenti.

Utilizza @babel/preset-env

La sintassi del linguaggio JavaScript è conforme a uno standard noto come ECMAScript o ECMA-262. Ogni anno vengono rilasciate nuove versioni della specifica che includono nuove funzionalità che hanno superato la procedura di proposta. Ogni browser principale è sempre in una fase diversa del supporto di queste funzionalità.

Nell'applicazione vengono utilizzate le seguenti funzionalità ES2015:

Viene utilizzata anche la seguente funzionalità ES2017:

Non esitare a esaminare il codice sorgente in src/index.js per scoprire come viene utilizzato.

Tutte queste funzionalità sono supportate nell'ultima versione di Chrome, ma che dire degli altri browser che non le supportano? Babel, incluso nell'applicazione, è la libreria più utilizzata per compilare codice contenente sintassi più recenti in codice che browser ed ambienti meno recenti possono comprendere. Lo fa in due modi:

  • I polyfill sono inclusi per emulare le funzioni ES2015 e successive più recenti in modo che le relative API possano essere utilizzate anche se non sono supportate dal browser. Ecco un esempio di 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 relative alla sintassi (ad esempio le funzioni freccia), non possono essere emulate con i polyfill.

Consulta 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 di Babel di base. In questo modo, tutte le configurazioni di Babel sono definite in un file .babelrc nella directory principale del progetto.
  • babel-loader include Babel nel processo di compilazione di webpack.

Ora guarda webpack.config.js per vedere come babel-loader è incluso come norma:

module: {
  rules: [
    //...
    {
      test: /\.js$/,
      exclude: /node_modules/,
      loader: "babel-loader"
    }
  ]
},
  • @babel/polyfill fornisce tutti i polyfill necessari per le funzionalità ECMAScript più recenti in modo che possano funzionare in ambienti che non le supportano. È già importato 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 viene incluso:

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

Questa è una configurazione di Babel e Webpack. Scopri come includere Babel nella tua applicazione se utilizzi un compilatore di moduli diverso da webpack.

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

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

Debug

Per avere un quadro completo di tutti i target Babel del browser, nonché di tutte le trasformazioni e i 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 ed esamina i log dello stato di Glitch nella parte inferiore dell'editor.

Browser scelti come target

Babel registra nella console una serie di dettagli relativi al processo di compilazione, inclusi tutti gli ambienti di destinazione per i quali è stato compilato il codice.

Browser scelti come target

Tieni presente che in questo elenco sono inclusi i browser non più disponibili, come Internet Explorer. Questo è un problema perché ai browser non supportati non verranno aggiunte le funzionalità più recenti e Babel continuerà a transpilare una sintassi specifica per questi browser. Ciò aumenta inutilmente le dimensioni del bundle se gli utenti non utilizzano questo browser per accedere al tuo sito.

Babel registra anche un elenco dei 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 ES2015 o successiva in una sintassi precedente per tutti i browser scelti come target.

Tuttavia, Babel non mostra i polyfill specifici utilizzati:

Nessun polyfill aggiunto

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

Carica i polyfill singolarmente

Per impostazione predefinita, Babel include tutti i polyfill necessari 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 dei polyfill importati

Anche se ora sono inclusi solo i polyfill necessari per "last 2 versions", l'elenco è ancora molto lungo. Questo perché i polyfill necessari per i browser di destinazione per ogni funzionalità più recente sono ancora inclusi. Modifica il valore dell'attributo in usage per includere solo quelli necessari per le funzionalità 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. Ciò significa che puoi rimuovere l'importazione di @babel/polyfill in src/index.js.

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

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

Elenco dei polyfill inclusi automaticamente

Le dimensioni del bundle dell'applicazione sono notevolmente ridotte.

Dimensioni del bundle ridotte a 30,1 KB

Restringere l'elenco dei browser supportati

Il numero di target del browser inclusi è ancora piuttosto elevato e non molti utenti utilizzano browser non più supportati 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.

Dimensioni del bundle pari a 30,0 KB

Poiché l'applicazione è così piccola, non c'è molta differenza con queste modifiche. Tuttavia, l'approccio consigliato è utilizzare una percentuale di quota di mercato del browser (ad es.">0.25%") ed escludere browser specifici che ritieni non siano utilizzati dai tuoi utenti. Per saperne di più, consulta l'articolo di James Kyle sulle "ultime due versioni" considerate dannose.

Utilizza <script type="module">

C'è ancora molto spazio per migliorare. Sebbene sia stata rimossa una serie di polyfill non utilizzati, ne vengono forniti molti che non sono necessari per alcuni browser. Con l'utilizzo dei moduli, è possibile scrivere e inviare direttamente ai browser una sintassi più recente senza utilizzare 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 importano ed esportano 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 negli ambienti che supportano i moduli JavaScript (anziché Babel). Ciò significa che il file Babel config può essere modificato per inviare due diverse versioni dell'applicazione al browser:

  • Una versione che funzioni nei browser più recenti che supportano i moduli e che includa un modulo in gran parte non transpiled, ma con dimensioni del file inferiori
  • Una versione che include uno script transpiled più grande che funzioni in qualsiasi browser precedente

Utilizzo di 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 di webpack specificando due diversi formati di compilazione per ogni versione dell'applicazione.

Per iniziare, aggiungi 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
}

Tieni presente che anziché utilizzare il valore targets per "@babel/preset-env", viene utilizzato esmodules con un valore di false. Ciò significa che Babel include tutte le trasformazioni e i polyfill necessari per avere come target ogni browser che non supporta ancora i moduli ES.

Aggiungi gli oggetti entry, cssRule e corePlugins all'inizio del webpack.config.js file. Questi sono condivisi sia con il modulo sia con gli script 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"})
];

Analogamente, crea un oggetto di configurazione per lo script del modulo di seguito in cui è 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 è che per il nome file di output viene utilizzata un'estensione file .mjs. Il valore esmodules è impostato su true, il che significa che il codice visualizzato in questo modulo è uno script più piccolo e meno compilato che non viene sottoposto a 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 viene creato sia un modulo più piccolo per i browser che lo supportano sia uno script transpiled 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 script contype="module". Ciò significa che puoi includere un modulo e un valore di riserva compilato. Idealmente, le due versioni dell'applicazione dovrebbero essere in index.html come questa:

<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 degli script regolari, gli script dei moduli vengono sempre differiti per impostazione predefinita. Se vuoi che anche lo script nomodule equivalente venga differito ed eseguito solo dopo la compilazione, devi aggiungere l'attributo defer:

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

L'ultima operazione da eseguire è aggiungere gli attributi module e nomodule rispettivamente al modulo e allo script precedente, importare il ScriptExtHtmlWebpackPlugin nella parte superiore 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 dello script .mjs e un attributo nomodule per tutti i moduli dello script .js.

Pubblicazione di moduli nel documento HTML

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

Aggiungi quanto segue 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 la versione più recente di Chrome.

Modulo di 5,2 KB recuperato tramite la rete per i browser più recenti

Viene recuperato solo il modulo, con un bundle molto più piccolo poiché è in gran parte non transpiled. L'altro elemento dello script viene completamente ignorato dal browser.

Se carichi l'applicazione su un browser meno recente, viene recuperato solo lo script transpiled più grande 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 di 30 KB recuperato per i browser meno recenti

Conclusione

Ora sai come utilizzare @babel/preset-env per fornire solo i polyfill necessari per i browser scelti come target. Inoltre, sai in che modo i moduli JavaScript possono migliorare ulteriormente il rendimento inviando due diverse versioni transpilate di un'applicazione. Ora che hai compreso in che modo entrambe queste tecniche possono ridurre notevolmente le dimensioni del bundle, non ti resta che ottimizzare.