Langfristiges Caching nutzen

Vorteile von Webpack beim Asset-Caching

Nach der Optimierung der App-Größe können Sie die Ladezeit der App durch Caching weiter verbessern. So können Sie Teile der App auf dem Client speichern und müssen sie nicht jedes Mal neu herunterladen.

Bundle-Versionierung und Cache-Header verwenden

Beim Caching wird in der Regel Folgendes getan:

  1. Sie können den Browser anweisen, eine Datei für einen sehr langen Zeitraum (z.B. ein Jahr) im Cache zu speichern:

    # Server header
    Cache-Control: max-age=31536000
    

    Wenn Sie nicht wissen, was Cache-Control tut, lesen Sie den ausgezeichneten Artikel von Jake Archibald zu Best Practices für das Caching.

  2. und benennen Sie die Datei um, wenn sie geändert wurde, um einen erneuten Download zu erzwingen:

    <!-- Before the change -->
    <script src="./index-v15.js"></script>
    
    <!-- After the change -->
    <script src="./index-v16.js"></script>
    

Bei diesem Ansatz wird der Browser angewiesen, die JS-Datei herunterzuladen, im Cache zu speichern und die im Cache gespeicherte Kopie zu verwenden. Der Browser greift nur dann auf das Netzwerk zu, wenn sich der Dateiname ändert (oder ein Jahr vergeht).

Bei webpack gehen Sie genauso vor, geben aber anstelle einer Versionsnummer den Datei-Hash an. Wenn Sie den Hash in den Dateinamen einfügen möchten, verwenden Sie [chunkhash]:

// webpack.config.js
module.exports = {
  entry: './index.js',
  output: {
    filename: 'bundle.[chunkhash].js' // → bundle.8e0d62a03.js
  }
};

Wenn Sie den Dateinamen zum Senden an den Client benötigen, verwenden Sie entweder HtmlWebpackPlugin oder WebpackManifestPlugin.

Die HtmlWebpackPlugin ist ein einfacher, aber weniger flexibler Ansatz. Während der Kompilierung generiert dieses Plug-in eine HTML-Datei mit allen kompilierten Ressourcen. Wenn Ihre Serverlogik nicht komplex ist, sollte das ausreichen:

<!-- index.html -->
<!DOCTYPE html>
<!-- ... -->
<script src="bundle.8e0d62a03.js"></script>

Die WebpackManifestPlugin ist ein flexiblerer Ansatz, der sich bei komplexen Serverteilen eignet. Während des Builds wird eine JSON-Datei mit einer Zuordnung zwischen Dateinamen ohne Hash und Dateinamen mit Hash generiert. Anhand dieser JSON-Datei auf dem Server kannst du herausfinden, mit welcher Datei du arbeiten möchtest:

// manifest.json
{
  "bundle.js": "bundle.8e0d62a03.js"
}

Weitere Informationen

Abhängigkeiten und Laufzeit in eine separate Datei extrahieren

Abhängigkeiten

App-Abhängigkeiten ändern sich in der Regel seltener als der eigentliche App-Code. Wenn Sie sie in eine separate Datei verschieben, kann der Browser sie separat im Cache speichern und muss sie nicht jedes Mal neu herunterladen, wenn sich der App-Code ändert.

Führen Sie die folgenden drei Schritte aus, um Abhängigkeiten in einen separaten Codeblock zu extrahieren:

  1. Ersetzen Sie den Ausgabedateinamen durch [name].[chunkname].js:

    // webpack.config.js
    module.exports = {
      output: {
        // Before
        filename: 'bundle.[chunkhash].js',
        // After
        filename: '[name].[chunkhash].js'
      }
    };
    

    Wenn webpack die App erstellt, ersetzt es [name] durch den Namen eines Chunks. Wenn wir den Teil [name] nicht hinzufügen, müssen wir die einzelnen Blöcke anhand ihres Hashwerts unterscheiden – was ziemlich schwierig ist.

  2. Wandeln Sie das Feld entry in ein Objekt um:

    // webpack.config.js
    module.exports = {
      // Before
      entry: './index.js',
      // After
      entry: {
        main: './index.js'
      }
    };
    

    In diesem Snippet ist „main“ der Name eines Chunks. Dieser Name wird anstelle von [name] aus Schritt 1 eingefügt.

    Wenn Sie die App jetzt erstellen, enthält dieser Code-Block den gesamten App-Code, als hätten wir diese Schritte nicht ausgeführt. Das ändert sich aber gleich.

  3. In webpack 4 fügen Sie die Option optimization.splitChunks.chunks: 'all' in die webpack-Konfiguration ein:

    // webpack.config.js (for webpack 4)
    module.exports = {
      optimization: {
        splitChunks: {
          chunks: 'all'
        }
      }
    };
    

    Mit dieser Option wird die intelligente Code-Aufteilung aktiviert. Damit würde webpack den Anbietercode extrahieren, wenn er größer als 30 KB ist (vor Minimierung und GZIP). Außerdem wird der gemeinsame Code extrahiert. Das ist nützlich, wenn Ihr Build mehrere Bundles generiert, z. B. wenn Sie Ihre App in Routen aufteilen.

    In webpack 3 fügen Sie CommonsChunkPlugin hinzu:

    // webpack.config.js (for webpack 3)
    module.exports = {
      plugins: [
        new webpack.optimize.CommonsChunkPlugin({
        // A name of the chunk that will include the dependencies.
        // This name is substituted in place of [name] from step 1
        name: 'vendor',
    
        // A function that determines which modules to include into this chunk
        minChunks: module => module.context && module.context.includes('node_modules'),
        })
      ]
    };
    

    Dieses Plug-in verschiebt alle Module, deren Pfade node_modules enthalten, in eine separate Datei namens vendor.[chunkhash].js.

Nach diesen Änderungen werden bei jedem Build zwei Dateien anstelle einer generiert: main.[chunkhash].js und vendor.[chunkhash].js (vendors~main.[chunkhash].js für Webpack 4). Bei Webpack 4 wird das Anbieter-Bundle möglicherweise nicht generiert, wenn die Abhängigkeiten gering sind. Das ist in Ordnung:

$ webpack
Hash: ac01483e8fec1fa70676
Version: webpack 3.8.1
Time: 3816ms
                        Asset      Size  Chunks             Chunk Names
 ./main.00bab6fd3100008a42b0.js   82 kB       0  [emitted]  main
./vendor.d9e134771799ecdf9483.js  47 kB       1  [emitted]  vendor

Der Browser würde diese Dateien separat im Cache speichern und nur Code herunterladen, der sich ändert.

Webpack-Laufzeitcode

Es reicht leider nicht aus, nur den Anbietercode zu extrahieren. Wenn Sie versuchen, etwas im App-Code zu ändern, geschieht Folgendes:

// index.js
…
…

// E.g. add this:
console.log('Wat');

Sie werden feststellen, dass sich auch der Hashwert für vendor ändert:

                           Asset   Size  Chunks             Chunk Names
./vendor.d9e134771799ecdf9483.js  47 kB       1  [emitted]  vendor

                            Asset   Size  Chunks             Chunk Names
./vendor.e6ea4504d61a1cc1c60b.js  47 kB       1  [emitted]  vendor

Das liegt daran, dass das Webpack-Bundle neben dem Code der Module eine Laufzeit enthält, also ein kleines Stück Code, das die Modulausführung verwaltet. Wenn Sie den Code in mehrere Dateien aufteilen, enthält dieser Code eine Zuordnung zwischen Chunk-IDs und entsprechenden Dateien:

// vendor.e6ea4504d61a1cc1c60b.js
script.src = __webpack_require__.p + chunkId + "." + {
    "0": "2f2269c7f0a55a5c1871"
}[chunkId] + ".js";

Webpack fügt diese Laufzeit in den letzten generierten Chunk ein, in unserem Fall vendor. Und jedes Mal, wenn sich ein Chunk ändert, ändert sich auch dieser Code, wodurch sich der gesamte vendor-Chunk ändert.

Verschieben wir die Laufzeit in eine separate Datei, um das Problem zu beheben. In webpack 4 wird dies durch Aktivieren der Option optimization.runtimeChunk erreicht:

// webpack.config.js (for webpack 4)
module.exports = {
  optimization: {
    runtimeChunk: true
  }
};

In webpack 3 erstellen Sie dazu ein zusätzliches leeres Chunk mit dem CommonsChunkPlugin:

// webpack.config.js (for webpack 3)
module.exports = {
  plugins: [
    new webpack.optimize.CommonsChunkPlugin({
      name: 'vendor',
      minChunks: module => module.context && module.context.includes('node_modules')
    }),
    // This plugin must come after the vendor one (because webpack
    // includes runtime into the last chunk)
    new webpack.optimize.CommonsChunkPlugin({
      name: 'runtime',
      // minChunks: Infinity means that no app modules
      // will be included into this chunk
      minChunks: Infinity
    })
  ]
};

Nach diesen Änderungen werden bei jedem Build drei Dateien generiert:

$ webpack
Hash: ac01483e8fec1fa70676
Version: webpack 3.8.1
Time: 3816ms
                            Asset     Size  Chunks             Chunk Names
   ./main.00bab6fd3100008a42b0.js    82 kB       0  [emitted]  main
 ./vendor.26886caf15818fa82dfa.js    46 kB       1  [emitted]  vendor
./runtime.79f17c27b335abc7aaf4.js  1.45 kB       3  [emitted]  runtime

Fügen Sie sie in umgekehrter Reihenfolge in index.html ein. Fertig:

<!-- index.html -->
<script src="./runtime.79f17c27b335abc7aaf4.js"></script>
<script src="./vendor.26886caf15818fa82dfa.js"></script>
<script src="./main.00bab6fd3100008a42b0.js"></script>

Weitere Informationen

Inline-Webpack-Laufzeitumgebung, um eine zusätzliche HTTP-Anfrage zu sparen

Noch besser ist es, die webpack-Laufzeit in die HTML-Antwort einzubetten. Anstelle von:

<!-- index.html -->
<script src="./runtime.79f17c27b335abc7aaf4.js"></script>

Gehen Sie dazu so vor:

<!-- index.html -->
<script>
!function(e){function n(r){if(t[r])return t[r].exports;…}} ([]);
</script>

Die Laufzeit ist gering und durch das Einfügen können Sie eine HTTP-Anfrage sparen. Das ist bei HTTP/1 relativ wichtig, bei HTTP/2 weniger, kann aber dennoch einen Effekt haben.

Und so geht's!

Wenn Sie HTML mit dem HtmlWebpackPlugin generieren

Wenn Sie das HtmlWebpackPlugin zum Generieren einer HTML-Datei verwenden, benötigen Sie nur das InlineSourcePlugin:

const HtmlWebpackPlugin = require('html-webpack-plugin');
const InlineSourcePlugin = require('html-webpack-inline-source-plugin');

module.exports = {
  plugins: [
    new HtmlWebpackPlugin({
      inlineSource: 'runtime~.+\\.js',
    }),
    new InlineSourcePlugin()
  ]
};

Wenn Sie HTML mit benutzerdefinierter Serverlogik generieren

Mit Webpack 4:

  1. Fügen Sie den WebpackManifestPlugin hinzu, um den generierten Namen des Laufzeit-Chunks zu erfahren:

    // webpack.config.js (for webpack 4)
    const ManifestPlugin = require('webpack-manifest-plugin');
    
    module.exports = {
      plugins: [
        new ManifestPlugin()
      ]
    };
    

    Bei einem Build mit diesem Plug-in wird eine Datei erstellt, die so aussieht:

    // manifest.json
    {
      "runtime~main.js": "runtime~main.8e0d62a03.js"
    }
    
  2. Sie können den Inhalt des Laufzeit-Chunks auf einfache Weise inline einfügen. Beispiel mit Node.js und Express:

    // server.js
    const fs = require('fs');
    const manifest = require('./manifest.json');
    const runtimeContent = fs.readFileSync(manifest['runtime~main.js'], 'utf-8');
    
    app.get('/', (req, res) => {
      res.send(`
        …
        <script>${runtimeContent}</script>
        …
      `);
    });
    

Oder mit Webpack 3:

  1. Legen Sie den Laufzeitnamen statisch fest, indem Sie filename angeben:

    module.exports = {
      plugins: [
        new webpack.optimize.CommonsChunkPlugin({
          name: 'runtime',
          minChunks: Infinity,
          filename: 'runtime.js'
        })
      ]
    };
    
  2. runtime.js-Inhalte können ganz einfach inline eingefügt werden. Beispiel mit Node.js und Express:

    // server.js
    const fs = require('fs');
    const runtimeContent = fs.readFileSync('./runtime.js', 'utf-8');
    
    app.get('/', (req, res) => {
      res.send(`
        …
        <script>${runtimeContent}</script>
        …
      `);
    });
    

Lazy-Load-Code, der derzeit nicht benötigt wird

Manchmal hat eine Seite mehr und weniger wichtige Teile:

  • Wenn du eine Videoseite auf YouTube aufrufst, geht es dir in erster Linie um das Video und nicht um die Kommentare. Hier ist das Video wichtiger als die Kommentare.
  • Wenn Sie einen Artikel auf einer Nachrichtenwebsite öffnen, interessieren Sie sich mehr für den Text des Artikels als für Werbung. Hier ist der Text wichtiger als Anzeigen.

In solchen Fällen können Sie die Ladeleistung beim ersten Laden verbessern, indem Sie zuerst nur die wichtigsten Inhalte herunterladen und die restlichen Teile später per Lazy Loading laden. Verwenden Sie dazu die import()-Funktion und die Code-Spaltung:

// videoPlayer.js
export function renderVideoPlayer() { … }

// comments.js
export function renderComments() { … }

// index.js
import {renderVideoPlayer} from './videoPlayer';
renderVideoPlayer();

// …Custom event listener
onShowCommentsClick(() => {
  import('./comments').then((comments) => {
    comments.renderComments();
  });
});

Mit import() geben Sie an, dass ein bestimmtes Modul dynamisch geladen werden soll. Wenn webpack import('./module.js') erkennt, verschiebt es dieses Modul in einen separaten Chunk:

$ webpack
Hash: 39b2a53cb4e73f0dc5b2
Version: webpack 3.8.1
Time: 4273ms
                            Asset     Size  Chunks             Chunk Names
      ./0.8ecaf182f5c85b7a8199.js  22.5 kB       0  [emitted]
   ./main.f7e53d8e13e9a2745d6d.js    60 kB       1  [emitted]  main
 ./vendor.4f14b6326a80f4752a98.js    46 kB       2  [emitted]  vendor
./runtime.79f17c27b335abc7aaf4.js  1.45 kB       3  [emitted]  runtime

und lädt sie erst herunter, wenn die Ausführung die import()-Funktion erreicht.

Dadurch wird das main-Bundle kleiner und die anfängliche Ladezeit verkürzt. Außerdem wird das Caching verbessert: Wenn Sie den Code im Haupt-Chunk ändern, hat das keine Auswirkungen auf den Kommentar-Chunk.

Weitere Informationen

Code in Routen und Seiten aufteilen

Wenn Ihre App mehrere Routen oder Seiten hat, aber nur eine JS-Datei mit dem Code (ein einzelner main-Chunk) vorhanden ist, werden bei jeder Anfrage wahrscheinlich zusätzliche Bytes gesendet. Wenn ein Nutzer beispielsweise die Startseite Ihrer Website besucht, geschieht Folgendes:

Startseite von Web Fundamentals

Sie müssen den Code zum Rendern eines Artikels, der sich auf einer anderen Seite befindet, nicht laden, tun dies aber trotzdem. Wenn der Nutzer immer nur die Startseite besucht und Sie eine Änderung am Artikelcode vornehmen, macht webpack das gesamte Bundle ungültig und der Nutzer muss die gesamte App noch einmal herunterladen.

Wenn wir die App in Seiten (oder Routen, wenn es sich um eine App mit einer Seite handelt) aufteilen, lädt der Nutzer nur den relevanten Code herunter. Außerdem wird der App-Code vom Browser besser im Cache gespeichert: Wenn Sie den Code der Startseite ändern, macht webpack nur den entsprechenden Codeblock ungültig.

Für Single-Page-Apps

Wenn Sie einseitige Apps nach Routen aufteilen möchten, verwenden Sie import() (siehe Abschnitt Lazy-Load-Code, der derzeit nicht benötigt wird). Wenn Sie ein Framework verwenden, gibt es möglicherweise eine vorhandene Lösung für dieses Problem:

Für herkömmliche mehrseitige Apps

Wenn Sie traditionelle Apps nach Seiten aufteilen möchten, verwenden Sie die Einstiegspunkte von webpack. Wenn Ihre App drei Arten von Seiten hat (Startseite, Artikelseite und Nutzerkontoseite), sollten Sie drei Einträge haben:

// webpack.config.js
module.exports = {
  entry: {
    home: './src/Home/index.js',
    article: './src/Article/index.js',
    profile: './src/Profile/index.js'
  }
};

Für jede Eingabedatei erstellt webpack ein separates Abhängigkeitsbaum und generiert ein Bundle, das nur Module enthält, die von diesem Eintrag verwendet werden:

$ webpack
Hash: 318d7b8490a7382bf23b
Version: webpack 3.8.1
Time: 4273ms
                            Asset     Size  Chunks             Chunk Names
      ./0.8ecaf182f5c85b7a8199.js  22.5 kB       0  [emitted]
   ./home.91b9ed27366fe7e33d6a.js    18 kB       1  [emitted]  home
./article.87a128755b16ac3294fd.js    32 kB       2  [emitted]  article
./profile.de945dc02685f6166781.js    24 kB       3  [emitted]  profile
 ./vendor.4f14b6326a80f4752a98.js    46 kB       4  [emitted]  vendor
./runtime.318d7b8490a7382bf23b.js  1.45 kB       5  [emitted]  runtime

Wenn also nur die Artikelseite Lodash verwendet, ist sie nicht im home- und profile-Bundle enthalten. Der Nutzer muss diese Bibliothek also nicht herunterladen, wenn er die Startseite besucht.

Separate Abhängigkeitsbäume haben jedoch auch Nachteile. Wenn zwei Einstiegspunkte Lodash verwenden und Sie Ihre Abhängigkeiten nicht in ein Anbieter-Bundle verschoben haben, enthalten beide Einstiegspunkte eine Kopie von Lodash. Fügen Sie dazu in Webpack 4 die Option optimization.splitChunks.chunks: 'all' in die Webpack-Konfiguration ein:

// webpack.config.js (for webpack 4)
module.exports = {
  optimization: {
    splitChunks: {
      chunks: 'all'
    }
  }
};

Mit dieser Option wird die intelligente Code-Aufteilung aktiviert. Mit dieser Option sucht webpack automatisch nach gemeinsamem Code und extrahiert ihn in separate Dateien.

Oder verwenden Sie in webpack 3 CommonsChunkPlugin. Dadurch werden gängige Abhängigkeiten in eine neue angegebene Datei verschoben:

module.exports = {
  plugins: [
    new webpack.optimize.CommonsChunkPlugin({
      name: 'common',
      minChunks: 2    // 2 is the default value
    })
  ]
};

Sie können den Wert für minChunks variieren, um den besten Wert zu finden. Im Allgemeinen sollten Sie sie klein halten, aber erhöhen, wenn die Anzahl der Chunks zunimmt. Bei drei Chunks kann minChunks beispielsweise 2 sein, bei 30 Chunks aber 8. Wenn Sie minChunks bei 2 belassen, werden zu viele Module in die gemeinsame Datei aufgenommen, wodurch sie zu groß wird.

Weitere Informationen

Modul-IDs stabiler machen

Beim Erstellen des Codes weist webpack jedem Modul eine ID zu. Diese IDs werden später in require()s im Bundle verwendet. In der Build-Ausgabe werden IDs normalerweise direkt vor den Modulpfaden angezeigt:

$ webpack
Hash: df3474e4f76528e3bbc9
Version: webpack 3.8.1
Time: 2150ms
                           Asset      Size  Chunks             Chunk Names
      ./0.8ecaf182f5c85b7a8199.js  22.5 kB       0  [emitted]
   ./main.4e50a16675574df6a9e9.js    60 kB       1  [emitted]  main
 ./vendor.26886caf15818fa82dfa.js    46 kB       2  [emitted]  vendor
./runtime.79f17c27b335abc7aaf4.js  1.45 kB       3  [emitted]  runtime

↓ Hier

[0] ./index.js 29 kB {1} [built]
[2] (webpack)/buildin/global.js 488 bytes {2} [built]
[3] (webpack)/buildin/module.js 495 bytes {2} [built]
[4] ./comments.js 58 kB {0} [built]
[5] ./ads.js 74 kB {1} [built]
+ 1 hidden module

Standardmäßig werden IDs mit einem Zähler berechnet (d. h. das erste Modul hat die ID 0, das zweite die ID 1 usw.). Das Problem dabei ist, dass ein neues Modul, das Sie hinzufügen, möglicherweise in der Mitte der Modulliste erscheint und dadurch die IDs aller nachfolgenden Module geändert werden:

$ webpack
Hash: df3474e4f76528e3bbc9
Version: webpack 3.8.1
Time: 2150ms
                           Asset      Size  Chunks             Chunk Names
      ./0.5c82c0f337fcb22672b5.js    22 kB       0  [emitted]
   ./main.0c8b617dfc40c2827ae3.js    82 kB       1  [emitted]  main
 ./vendor.26886caf15818fa82dfa.js    46 kB       2  [emitted]  vendor
./runtime.79f17c27b335abc7aaf4.js  1.45 kB       3  [emitted]  runtime
   [0] ./index.js 29 kB {1} [built]
   [2] (webpack)/buildin/global.js 488 bytes {2} [built]
   [3] (webpack)/buildin/module.js 495 bytes {2} [built]

↓ Wir haben neue Module hinzugefügt…

[4] ./webPlayer.js 24 kB {1} [built]

↓ Und sieh dir an, was passiert ist! comments.js hat jetzt die ID 5 statt 4.

[5] ./comments.js 58 kB {0} [built]

↓ ads.js hat jetzt die ID 6 statt 5

[6] ./ads.js 74 kB {1} [built]
       + 1 hidden module

Dadurch werden alle Chunks ungültig, die Module mit geänderten IDs enthalten oder davon abhängig sind – auch wenn sich der Code nicht geändert hat. In unserem Fall werden der 0-Chunk (der Chunk mit comments.js) und der main-Chunk (der Chunk mit dem anderen App-Code) ungültig, obwohl nur der main-Chunk ungültig sein sollte.

Ändern Sie dazu mithilfe der HashedModuleIdsPlugin, wie Modul-IDs berechnet werden. Dabei werden zählerbasierte IDs durch Hashes von Modulpfaden ersetzt:

$ webpack
Hash: df3474e4f76528e3bbc9
Version: webpack 3.8.1
Time: 2150ms
                           Asset      Size  Chunks             Chunk Names
      ./0.6168aaac8461862eab7a.js  22.5 kB       0  [emitted]
   ./main.a2e49a279552980e3b91.js    60 kB       1  [emitted]  main
 ./vendor.ff9f7ea865884e6a84c8.js    46 kB       2  [emitted]  vendor
./runtime.25f5d0204e4f77fa57a1.js  1.45 kB       3  [emitted]  runtime

↓ Hier

[3IRH] ./index.js 29 kB {1} [built]
[DuR2] (webpack)/buildin/global.js 488 bytes {2} [built]
[JkW7] (webpack)/buildin/module.js 495 bytes {2} [built]
[LbCc] ./webPlayer.js 24 kB {1} [built]
[lebJ] ./comments.js 58 kB {0} [built]
[02Tr] ./ads.js 74 kB {1} [built]
    + 1 hidden module

Bei diesem Ansatz ändert sich die ID eines Moduls nur, wenn Sie es umbenennen oder verschieben. Neue Module wirken sich nicht auf die IDs anderer Module aus.

Fügen Sie das Plug-in zum Abschnitt plugins der Konfiguration hinzu, um es zu aktivieren:

// webpack.config.js
module.exports = {
  plugins: [
    new webpack.HashedModuleIdsPlugin()
  ]
};

Weitere Informationen

Zusammenfassung

  • Setzen Sie das Bundle in den Cache und unterscheiden Sie zwischen Versionen, indem Sie den Bundle-Namen ändern.
  • Das Bundle in App-Code, Anbietercode und Laufzeit aufteilen
  • Laufzeit einfügen, um eine HTTP-Anfrage zu speichern
  • Nicht kritischen Code mit import per Lazy Load laden
  • Code nach Routen/Seiten aufteilen, um unnötiges Laden zu vermeiden