Korzystanie z długoterminowego buforowania

Jak pakiet internetowy pomaga buforować zasoby

Następnie (po zoptymalizowaniu rozmiaru aplikacji, skraca czas wczytywania aplikacji to buforowanie. Dzięki niemu niektóre części aplikacji będą dostępne klienta i unikać ich ponownego pobierania za każdym razem.

Użyj obsługi wersji pakietów i nagłówków pamięci podręcznej

Typowym sposobem korzystania z buforowania jest:

  1. poinstruuj przeglądarkę, aby zapisywała plik w pamięci podręcznej przez bardzo długi okres (np. rok):

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

    Jeśli nie wiesz, co robi Cache-Control, zobacz film Jake'a Archibalda świetny post na buforowanie najlepsze metod Google Play.

  2. i zmień nazwę pliku, aby wymusić ponowne pobieranie:

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

Dzięki temu przeglądarka może pobrać plik JS, zapisać go w pamięci podręcznej i użyć kopii zapasowej. Przeglądarka łączy się z siecią tylko w przypadku zmiany nazwy pliku (lub jeśli minął rok).

W przypadku Webpacka robisz to samo, ale zamiast numeru wersji podajesz hasz pliku. Aby umieścić hasz w nazwie pliku, użyj [chunkhash]:

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

Jeśli potrzebujesz nazwę pliku, aby wysłać go do klienta, użyj HtmlWebpackPlugin lub WebpackManifestPlugin

HtmlWebpackPlugin to proste, ale mniej elastyczne. Podczas kompilacji ta wtyczka generuje Plik HTML zawierający wszystkie skompilowane zasoby. Jeśli logika Twojego serwera nie jest to powinno wystarczyć:

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

WebpackManifestPlugin to bardziej elastyczne podejście, które jest przydatne w przypadku złożonych elementów serwera. Podczas kompilacji generowany jest plik JSON z mapowaniem między nazwami plików bez haszy i nazw plików z haszem. Użyj tego pliku JSON na serwerze, aby się dowiedzieć wybrać odpowiedni plik:

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

Więcej informacji

Wyodrębnij zależności i środowisko wykonawcze do oddzielnego pliku

Zależności

Zależności aplikacji zmieniają się rzadziej niż rzeczywisty kod aplikacji. Jeśli przeprowadzisz się do osobnego pliku, przeglądarka może zapisać je w pamięci podręcznej oddzielnie, i nie będą pobierać ich ponownie po każdej zmianie kodu aplikacji.

Aby wyodrębnić zależności do osobnego fragmentu, wykonaj 3 kroki:

  1. Zastąp nazwę pliku wyjściowego [name].[chunkname].js:

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

    Podczas tworzenia aplikacji pakiet internetowy zastępuje [name] i nazwą fragmentu. Jeśli nie dodamy części [name], rozróżniania fragmentów według skrótu, co jest dość trudne.

  2. Przekonwertuj pole entry na obiekt:

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

    W tym fragmencie kodu „main” to nazwa fragmentu. Ta nazwa zostanie zastąpiona w miejsce [name] z kroku 1.

    Jeśli stworzysz aplikację, ten fragment będzie zawierał cały kod aplikacji – jak nie wykonaliśmy jeszcze tych czynności. Ale to się za chwilę zmieni.

  3. W pakiecie webpack 4 dodaj opcję optimization.splitChunks.chunks: 'all'. do konfiguracji pakietu internetowego:

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

    Ta opcja umożliwia inteligentne dzielenie kodu. Dzięki niemu Webpack wyodrębniłby kod dostawcy, jeśli większe niż 30 kB (przed minifikacją i gzip). Wyodrębniłby też wspólny kod – przydaje się to, gdy konstrukcja obejmuje kilka pakietów (np. jeśli aplikacja została podzielona na trasy).

    W pakiecie webpack 3 dodaj CommonsChunkPlugin:

    // 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'),
        })
      ]
    };
    

    Ta wtyczka bierze pod uwagę wszystkie moduły, które zawierają ścieżki node_modules i przeniesie je do osobnego pliku o nazwie vendor.[chunkhash].js.

Po wprowadzeniu tych zmian każda kompilacja wygeneruje 2 pliki zamiast jednego: main.[chunkhash].js oraz vendor.[chunkhash].js (vendors~main.[chunkhash].js w przypadku pakietu internetowego 4). W przypadku pakietu webpack 4: pakiet dostawców może nie zostać wygenerowany, jeśli zależności są małe.

$ 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

Przeglądarka zapisze te pliki oddzielnie w pamięci podręcznej i ponownie pobierze tylko zmieniony kod.

Kod środowiska wykonawczego Webpack

Wyodrębnienie samego kodu dostawcy nie wystarcza. Jeśli spróbujesz zmień coś w kodzie aplikacji:

// index.js
…
…

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

zauważysz, że wartość hash vendor również się zmieni:

                           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

Dzieje się tak, ponieważ pakiet Webpack, oprócz kodu modułów, ma aspekt (środowisko wykonawcze) – krótki fragment kodu. który zarządza wykonaniem modułu. Gdy podzielisz kod na kilka plików, Ten fragment kodu zawiera mapowanie między identyfikatorami fragmentów odpowiadające im pliki:

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

Webpack zawiera to środowisko wykonawcze w ostatnim wygenerowanym fragmencie, czyli vendor w naszym przypadku. Za każdym razem, gdy zmienia się jakiś fragment, ten fragment kodu też się zmienia, powodując zmianę całego fragmentu vendor.

Aby rozwiązać ten problem, przenieśmy środowisko wykonawcze do osobnego pliku. W pakiecie webpack 4 jest to: co można osiągnąć przez włączenie opcji optimization.runtimeChunk:

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

W pakiecie webpack 3 utwórz dodatkowy pusty fragment za pomocą pakietu 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
    })
  ]
};

Po wprowadzeniu tych zmian każda kompilacja będzie generować 3 pliki:

$ 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

Dodaj je do elementu index.html w odwrotnej kolejności – i gotowe:

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

Więcej informacji

Wbudowane środowisko wykonawcze pakietu internetowego pozwalające zapisać dodatkowe żądanie HTTP

Aby jeszcze bardziej ulepszyć usługę, umieść środowisko wykonawcze pakietu internetowego w kodzie HTML . To znaczy, że:

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

wykonaj następujące czynności:

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

Środowisko wykonawcze jest małe, a wbudowanie go pomoże zapisać żądanie HTTP (ładnie jest ważne w przypadku protokołu HTTP/1, mniej ważne w przypadku HTTP/2, ale nadal może odtwarzać efekt).

Oto jak to zrobić.

W przypadku generowania kodu HTML przy użyciu dodatku htmlWebpackPlugin

Jeśli używasz tagu HtmlWebpackPlugin do wygenerowania pliku HTML, InlineSourcePlugin wystarczy Ci:

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

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

Jeśli generujesz kod HTML przy użyciu niestandardowej logiki serwera

W przypadku pakietu webpack 4:

  1. Dodaj parametr WebpackManifestPlugin aby poznać wygenerowaną nazwę fragmentu środowiska wykonawczego:

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

    Kompilacja z tą wtyczką mogłaby spowodować utworzenie pliku podobnego do tego:

    // manifest.json
    {
      "runtime~main.js": "runtime~main.8e0d62a03.js"
    }
    
  2. Możesz w wygodny sposób umieścić treść fragmentu środowiska wykonawczego. Na przykład: Node.js i 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>
        …
      `);
    });
    

Lub w pakiecie webpack 3:

  1. Ustaw statyczną nazwę środowiska wykonawczego, określając filename:

    module.exports = {
      plugins: [
        new webpack.optimize.CommonsChunkPlugin({
          name: 'runtime',
          minChunks: Infinity,
          filename: 'runtime.js'
        })
      ]
    };
    
  2. Umieść w tekście treści runtime.js w wygodny sposób. Na przykład: Node.js i Express:

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

leniwe ładowanie kodu, którego teraz nie potrzebujesz;

Czasami strona zawiera więcej lub mniej ważne elementy:

  • Jeśli wczytasz w YouTube stronę z filmem, bardziej zależy Ci na nim niż na jego treści komentarzy. W tym przypadku film jest ważniejszy niż komentarze.
  • Jeśli otwierasz artykuł w witrynie z wiadomościami, bardziej zależy Ci na tekście niż o reklamach. W tym przypadku tekst jest ważniejszy niż reklamy.

W takich przypadkach możesz poprawić wydajność wczytywania początkowego, pobierając W pierwszej kolejności to, co najważniejsze, a pozostałe w późniejszym czasie. Za pomocą funkcji import() i code-splitting w tym celu:

// 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() określa, że określony moduł ma być ładowany dynamicznie. Kiedy Webpack widzi adres import('./module.js'), przenosi go do osobnego 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

i pobiera go dopiero wtedy, gdy wykonanie dotrze do funkcji import().

Spowoduje to skrócenie pakietu main i skrócenie początkowego czasu wczytywania. Co więcej, usprawni to buforowanie – jeśli zmienisz kod we fragmencie głównym, nie wpłynie na fragment komentarzy.

Więcej informacji

Podziel kod na trasy i strony

Jeśli aplikacja ma wiele tras lub stron, ale tylko jeden plik JS z (pojedynczy fragment main) prawdopodobnie wykorzystujesz dodatkowe bajty każdego żądania. Na przykład, gdy użytkownik odwiedza stronę główną w Twojej witrynie:

Strona główna WebFundamentals

nie muszą wczytywać kodu, by wyrenderować artykuł znajdujący się na innym ale zostaną wczytane. Co więcej, jeśli użytkownik zawsze odwiedza tylko stronę główną i wprowadzisz zmianę w kodzie artykułu, pakiet webpack unieważni całego pakietu, a użytkownik będzie musiał jeszcze raz pobrać całą aplikację.

Jeśli podzielisz aplikację na strony (lub trasy, jeśli jest to aplikacja jednostronicowa), użytkownik pobierze tylko właściwy kod. Dodatkowo przeglądarka zapisze kod aplikacji w pamięci podręcznej Lepiej: jeśli zmienisz kod strony głównej, pakiet internetowy unieważni tylko odpowiadający mu fragment.

Aplikacje jednostronicowe

Aby podzielić aplikacje jednostronicowe według trasy, użyj funkcji import() (patrz „Kod leniwego ładowania” których obecnie nie potrzebujesz”). Jeśli korzystasz z platformy, może istnieć rozwiązanie tego problemu:

Tradycyjne aplikacje wielostronicowe

Aby podzielić tradycyjne aplikacje na strony, użyj wpisu z pakietu webpack punktów. Jeśli aplikacja ma trzy różne rodzaje stron: strona główna, strona z artykułem i strona konta użytkownika; powinien zawierać 3 pozycje:

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

Dla każdego pliku wpisu pakiet internetowy utworzy osobne drzewo zależności i wygeneruje pakiet zawierający tylko moduły, które są używane w danym wpisie:

$ 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

Jeśli więc tylko strona artykułu używa Lodash, pakietów home i profile nie zostaną uwzględnione i użytkownik nie będzie musiał pobierać tej biblioteki, odwiedzający stronę główną.

Osobne drzewa zależności mają jednak wady. Jeśli dwa punkty wejścia używają Lodash i nie przeniosłeś(-aś) zależności do pakietu dostawców – oba wpisy punkty będą zawierały kopię hasła Lodash. Aby rozwiązać ten problem, w pakiecie webpack 4 dodaj parametr Opcja optimization.splitChunks.chunks: 'all' do konfiguracji pakietu internetowego:

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

Ta opcja umożliwia inteligentne dzielenie kodu. W przypadku tej opcji pakiet internetowy automatycznie wyszukać wspólny kod i wyodrębnić go do osobnych plików.

Możesz też w webpack 3 użyć CommonsChunkPlugin. – przeniesie wspólne zależności do nowego określonego pliku:

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

Możesz wypróbować wartość minChunks, aby znaleźć najlepszą opcję. Ogólnie rzecz biorąc, niech będzie mały, ale zwiększ, jeśli ich liczba wzrośnie. Dla: na przykład dla 3 fragmentów minChunks może wynosić 2, ale dla 30 części może wynosić 8 bo jeśli w niej będzie 2, zbyt wiele modułów będzie miało dostęp do wspólnego pliku, nie nadużywając go.

Więcej informacji

Zwiększ stabilność identyfikatorów modułów

Podczas tworzenia kodu pakiet internetowy przypisuje każdemu modułowi identyfikator. Później te identyfikatory są używane w elementach require() w pakiecie. Identyfikatory zwykle znajdują się w danych wyjściowych kompilacji tuż przed ścieżkami modułów:

$ 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

↓ Tutaj

[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

Domyślnie identyfikatory są obliczane za pomocą licznika (tzn. pierwszy moduł ma identyfikator 0, drugi ma identyfikator 1 itd.). Problem polega na tym, że podczas dodawania nowego modułu, może on pojawić się na środku listy modułów i zmieniać wszystkie Identyfikatory:

$ 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]

↓ Dodaliśmy nowe moduł...

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

↓ I spójrz, co udało Ci się osiągnąć! comments.js ma teraz identyfikator 5 zamiast 4

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

ads.js ma teraz identyfikator 6 zamiast 5

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

Spowoduje to unieważnienie wszystkich fragmentów, które zawierają moduły ze zmienionymi identyfikatorami lub są od nich zależne: nawet jeśli ich faktyczny kod się nie zmienił. W naszym przypadku fragment 0 (fragment) (comments.js), a fragment main (fragment z drugim kodem aplikacji) otrzymuje unieważnione – podczas gdy powinno było być tylko main.

Aby rozwiązać ten problem, zmień sposób obliczania identyfikatorów modułów za pomocą metody HashedModuleIdsPlugin Zastępuje identyfikatory oparte na licznikach haszami ścieżek modułów:

$ 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

↓ Tutaj

[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

Przy tej metodzie identyfikator modułu zmienia się tylko wtedy, gdy zmienisz jego nazwę lub przeniesiesz go . Nowe moduły nie będą miały wpływu na inne moduły Identyfikatory.

Aby włączyć wtyczkę, dodaj ją do sekcji plugins konfiguracji:

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

Więcej informacji

Podsumowanie

  • Umieść pakiet w pamięci podręcznej i rozróżnij wersje, zmieniając nazwę pakietu
  • Podziel pakiet na kod aplikacji, kod dostawcy i środowisko wykonawcze
  • Wbudowane środowisko wykonawcze umożliwiające zapisanie żądania HTTP
  • Leniwe ładowanie niekrytycznego kodu za pomocą funkcji import
  • Podziel kod według tras lub stron, aby uniknąć wczytywania niepotrzebnych treści