Langfristiges Caching nutzen

So unterstützt Webpack das Asset-Caching

Der nächste Schritt (nach der Optimierung der Anwendungsgröße, der die Ladezeit der Anwendung verbessert) ist das Caching. Damit können Sie Teile der App im Client belassen und vermeiden, dass sie jedes Mal neu heruntergeladen werden müssen.

Bundle-Versionsverwaltung und Cache-Header verwenden

Der übliche Ansatz für das Caching lautet:

  1. 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 Jake Archibalds hervorragenden Beitrag zu Best Practices für das Caching.

  2. und benennen Sie die Datei nach der Änderung um, um einen erneuten Download zu erzwingen:

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

Dadurch 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 vergangen ist).

Bei Webpack geht das genauso, allerdings geben Sie anstelle einer Versionsnummer den Datei-Hash an. Verwenden Sie [chunkhash], um den Hash in den Dateinamen aufzunehmen:

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

Wenn der Dateiname zum Senden an den Client erforderlich ist, verwenden Sie entweder HtmlWebpackPlugin oder WebpackManifestPlugin.

HtmlWebpackPlugin ist ein einfacher, aber weniger flexibler Ansatz. Während der Kompilierung generiert dieses Plug-in eine HTML-Datei, die alle kompilierten Ressourcen enthält. Wenn Ihre Serverlogik nicht komplex ist, sollte sie für Sie ausreichen:

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

WebpackManifestPlugin ist ein flexiblerer Ansatz, der nützlich ist, wenn Ihre Server komplex sind. Während des Builds wird eine JSON-Datei mit einer Zuordnung zwischen Dateinamen ohne Hash und Dateinamen mit Hashwert generiert. Verwenden Sie diese JSON-Datei auf dem Server, um herauszufinden, mit welcher Datei Sie arbeiten möchten:

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

Weitere Informationen

Abhängigkeiten und Laufzeit in eine separate Datei extrahieren

Abhängigkeiten

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

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

  1. Ersetzen Sie den Namen der Ausgabedatei durch [name].[chunkname].js:

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

    Wenn Webpack die Anwendung erstellt, wird [name] durch den Namen eines Chunks ersetzt. Wenn wir den [name]-Teil nicht hinzufügen, müssen wir die einzelnen Blöcke nach ihrem Hash unterscheiden. Das ist ziemlich schwierig.

  2. Konvertieren Sie das Feld entry in ein Objekt:

    // 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 durch [name] aus Schritt 1 ersetzt.

    Wenn Sie die Anwendung erstellen, enthält dieser Block inzwischen den gesamten Anwendungscode, so wie wir diese Schritte nicht durchgeführt haben. Das ändert sich aber in einer Sekunde.

  3. Füge in Webpack 4 die Option optimization.splitChunks.chunks: 'all' in deine Webpack-Konfiguration ein:

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

    Diese Option ermöglicht eine intelligente Codeaufteilung. Damit extrahiert Webpack den Anbietercode, wenn er größer als 30 KB wird (vor der Reduzierung und gzip). Außerdem würde der gemeinsame Code extrahiert werden. Dies ist nützlich, wenn Ihr Build mehrere Bundles erzeugt (z. B. wenn Sie Ihre Anwendung in Routen aufteilen).

    Fügen Sie in Webpack 3 das 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 nimmt alle Module, deren Pfade node_modules enthalten, und verschiebt sie in eine separate Datei namens vendor.[chunkhash].js.

Nach diesen Änderungen generiert jeder Build zwei Dateien statt einer: 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 klein sind – und 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, der sich ändert, noch einmal herunterladen.

Webpack-Laufzeitcode

Leider reicht es nicht aus, nur den Anbietercode zu extrahieren. Wenn Sie versuchen, etwas im App-Code zu ändern:

// index.js
…
…

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

Sie sehen, dass sich auch der vendor-Hashwert ä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

Dies liegt daran, dass das Webpack-Bundle neben dem Code der Module eine Laufzeit hat – ein kleines Code-Snippet, das die Modulausführung verwaltet. Wenn Sie den Code in mehrere Dateien aufteilen, beginnt dieser Code, eine Zuordnung zwischen Chunk-IDs und den entsprechenden Dateien zu erstellen:

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

Webpack fügt diese Laufzeit in den letzten generierten Block ein, in unserem Fall vendor. Bei jeder Änderung eines Blocks ändert sich auch dieser Code, sodass sich der gesamte vendor-Chunk ändert.

Um dieses Problem zu lösen, verschieben wir die Laufzeit in eine separate Datei. In Webpack 4 wird dies durch Aktivieren der Option optimization.runtimeChunk erreicht:

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

Dazu erstellen Sie in Webpack 3 einen zusätzlichen leeren Block mit 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 generiert jeder Build drei Dateien:

$ 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 zu index.html ein – und schon sind Sie 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-Laufzeit zum Speichern einer zusätzlichen HTTP-Anfrage

Zur Optimierung dieser Funktion können Sie die Webpack-Laufzeit in die HTML-Antwort inline einbinden. Beispiel:

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

Vorgehensweise:

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

Die Laufzeit ist klein und hilft Ihnen beim Speichern einer HTTP-Anfrage, was bei HTTP/1 sehr wichtig ist, bei HTTP/2 weniger wichtig, aber möglicherweise trotzdem einen Effekt hat.

Und so geht's!

Wenn Sie HTML mit dem HTMLWebpackPlugin-

Wenn Sie zum Generieren einer HTML-Datei das HtmlWebpackPlugin verwenden, benötigen Sie lediglich 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 einer benutzerdefinierten Serverlogik generieren

Mit Webpack 4:

  1. Fügen Sie WebpackManifestPlugin hinzu, um den generierten Namen des Laufzeitblocks zu ermitteln:

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

    Bei einem Build mit diesem Plug-in würde die Datei so aussehen:

    // manifest.json
    {
      "runtime~main.js": "runtime~main.8e0d62a03.js"
    }
    
  2. Fügen Sie den Inhalt des Laufzeitblocks bequem inline in die Anzeige ein. 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. Machen Sie den Laufzeitnamen statisch, indem Sie filename angeben:

    module.exports = {
      plugins: [
        new webpack.optimize.CommonsChunkPlugin({
          name: 'runtime',
          minChunks: Infinity,
          filename: 'runtime.js'
        })
      ]
    };
    
  2. Bringe den runtime.js-Inhalt auf eine für sie geeignete Weise vor. 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-Loading-Code, den Sie im Moment nicht benötigen

Manchmal besteht eine Seite aus mehr und weniger wichtigen Teilen:

  • Wenn du eine Videoseite auf YouTube lädst, ist dir das Video wichtiger als Kommentare. In diesem Fall ist das Video wichtiger als Kommentare.
  • Wenn du einen Artikel auf einer Nachrichtenwebsite öffnest, ist dir der Text des Artikels wichtiger als die Werbung. Hier ist der Text wichtiger als Anzeigen.

Verbessern Sie in solchen Fällen die anfängliche Ladeleistung, indem Sie zuerst nur die wichtigsten Inhalte herunterladen und die restlichen Teile später per Lazy Loading laden. Verwenden Sie dazu die Funktion import() und die Codeaufteilung:

// 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();
  });
});

import() gibt an, dass ein bestimmtes Modul dynamisch geladen werden soll. Wenn Webpack import('./module.js') erkennt, wird dieses Modul in einen separaten Block verschoben:

$ 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 nur herunter, wenn sie die Funktion import() erreicht.

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

Weitere Informationen

Code in Routen und Seiten aufteilen

Wenn Ihre Anwendung mehrere Routen oder Seiten hat, es aber nur eine einzige JS-Datei mit dem Code (ein einzelner main-Chunk) gibt, stellen Sie wahrscheinlich für jede Anfrage zusätzliche Byte bereit. Angenommen, ein Nutzer besucht die Startseite Ihrer Website:

Eine WebFundamentals-Startseite

müssen sie den Code für das Rendern eines Artikels auf einer anderen Seite nicht laden, sondern laden ihn. Wenn der Nutzer immer nur die Startseite besucht und du eine Änderung am Artikelcode vornimmst, wird Webpack darüber hinaus das gesamte Bundle ungültig – und der Nutzer muss die gesamte App neu herunterladen.

Wenn wir die Anwendung in Seiten (oder Routen, falls es sich um eine App mit einer Seite handelt) aufteilen, lädt der Nutzer nur den relevanten Code herunter. Außerdem speichert der Browser den App-Code besser im Cache: Wenn Sie den Homepage-Code ändern, wird Webpack nur den entsprechenden Chunk ungültig machen.

Für Apps mit einer Seite

Wenn Sie einseitige Apps nach Routen aufteilen möchten, verwenden Sie import() (siehe Abschnitt Lazy-Load-Code, den Sie im Moment nicht benötigen). Wenn Sie ein Framework verwenden, gibt es dafür möglicherweise bereits eine Lösung:

Für herkömmliche mehrseitige Apps

Mit den Einstiegspunkten von Webpack können Sie traditionelle Anwendungen nach Seiten aufteilen. Wenn Ihre Anwendung drei Arten von Seiten hat: die Startseite, die Artikelseite und die Seite des Nutzerkontos, sollten drei Einträge vorhanden sein:

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

Für jede Eintragsdatei erstellt Webpack eine separate Abhängigkeitsstruktur 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 Lodash also nur auf der Artikelseite verwendet wird, ist es nicht in den Paketen home und profile enthalten. Außerdem muss der Nutzer diese Bibliothek beim Besuch der Startseite nicht herunterladen.

Separate Abhängigkeitsstrukturen haben jedoch ihre 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üge in Webpack 4 die Option optimization.splitChunks.chunks: 'all' in die Webpack-Konfiguration ein, um dieses Problem zu beheben:

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

Diese Option ermöglicht eine intelligente Codeaufteilung. Bei dieser Option sucht Webpack automatisch nach gängigem Code und extrahiert ihn in separate Dateien.

Alternativ kannst du in Webpack 3 CommonsChunkPlugin verwenden. Dadurch werden allgemeine 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 gerne mit dem Wert von minChunks experimentieren, um das beste zu finden. Grundsätzlich sollten Sie klein sein, aber erhöhen, wenn die Anzahl der Blöcke zunimmt. Bei drei Blöcken kann minChunks beispielsweise 2 sein, bei 30 Blöcken kann es aber 8 sein. Wenn Sie den Wert bei 2 halten, landen zu viele Module in der gemeinsamen Datei und erhöhen die Menge zu stark.

Weitere Informationen

Modul-IDs stabiler machen

Beim Erstellen des Codes weist Webpack jedem Modul eine ID zu. Später werden diese IDs in require()s innerhalb des Bundles verwendet. In der Regel werden IDs in der Build-Ausgabe 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 Modul die ID 1 usw.). Das Problem dabei ist, dass ein neues Modul, das Sie hinzufügen, möglicherweise in der Mitte der Modulliste angezeigt wird, wodurch die IDs aller nächsten 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 ein neues Modul hinzugefügt...

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

↓ Und sieh dir an, was damit erreicht wurde! 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 Blöcke entwertet, die Module mit geänderten IDs enthalten oder von diesen abhängig sind – auch wenn ihr Code sich nicht geändert hat. In unserem Fall werden der Chunk 0 (der Block mit comments.js) und der Chunk main (der Block mit dem anderen Anwendungscode) ungültig gemacht – während nur der Chunk main hätte sein sollen.

Ändern Sie zur Behebung dieses Problems die Berechnung von Modul-IDs mithilfe von HashedModuleIdsPlugin. Sie ersetzt zählerbasierte IDs durch Hashes von Modulpfaden:

$ 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 das Modul umbenennen oder verschieben. Neue Module haben keine Auswirkungen auf die IDs anderer Module.

Um das Plug-in zu aktivieren, fügen Sie es dem Abschnitt plugins der Konfiguration hinzu:

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

Weitere Informationen

Zusammenfassung

  • Das Bundle im Cache speichern und durch Ändern des Bundle-Namens zwischen Versionen unterscheiden
  • Bundle in Anwendungscode, Anbietercode und Laufzeit aufteilen
  • Laufzeit zum Speichern einer HTTP-Anfrage inline speichern
  • Lazy Loading von nicht kritischem Code mit import
  • Code nach Routen/Seiten aufteilen, um unnötiges Laden zu vermeiden