Korzystanie z długoterminowego buforowania

Jak pakiet internetowy pomaga w buforowaniu zasobów

Kolejną czynnością (po zoptymalizowaniu rozmiaru aplikacji, która skraca czas wczytywania aplikacji), jest zapisywanie w pamięci podręcznej. Używaj go, by zapisać fragmenty aplikacji w kliencie i unikać ich ponownego pobierania.

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

Typowym podejściem do buforowania jest:

  1. poinformować przeglądarkę, że ma zapisać plik w pamięci podręcznej na dłuższy czas (np. rok):

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

    Jeśli nie wiesz, jak działa Cache-Control, przeczytaj znakomity post Jake'a Archibalda na temat sprawdzonych metod buforowania.

  2. i zmień nazwę pliku po wprowadzeniu tej zmiany, aby wymusić ponowne pobranie:

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

Takie podejście informuje przeglądarkę, że ma pobrać plik JS, umieścić go w pamięci podręcznej i wykorzystać kopię przechowywaną w pamięci podręcznej. Przeglądarka łączy się z siecią tylko wtedy, gdy nazwa pliku się zmieni (lub gdy minie rok).

W przypadku pakietu internetowego robisz to samo, ale zamiast numeru wersji podajesz identyfikator pliku. Aby dołączyć hasz do nazwy pliku, użyj [chunkhash]:

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

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

HtmlWebpackPlugin to proste, ale mniej elastyczne podejście. Podczas kompilacji wtyczka generuje plik HTML ze wszystkimi skompilowanymi zasobami. Jeśli logika serwera nie jest skomplikowana, powinna wystarczyć:

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

WebpackManifestPlugin to bardziej elastyczne rozwiązanie, które jest przydatne w przypadku złożonej części serwerowej. Podczas kompilacji generuje plik JSON z mapowaniem między nazwami plików bez haszowania i nazwami z krzyżykiem. Użyj tego kodu JSON na serwerze, aby dowiedzieć się, z którym plikiem pracować:

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

Więcej informacji

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

Zależności

Zależności aplikacji zmieniają się rzadziej niż rzeczywisty kod aplikacji. Jeśli przeniesiesz je do osobnego pliku, przeglądarka będzie mogła zapisywać je w pamięci podręcznej oddzielnie i nie będzie ich ponownie pobierać 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 nazwą [name].[chunkname].js:

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

    Podczas tworzenia aplikacji przez pakiet webpack zastępuje [name] nazwą fragmentu. Jeśli nie dodamy części [name], trzeba będzie odróżniać fragmenty za pomocą skrótu – to całkiem 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 „główny” to nazwa fragmentu. Ta nazwa zostanie zastąpiona nazwą [name] z kroku 1.

    Jeśli utworzysz aplikację, ten fragment będzie zawierać cały jej kod – tak jak w przypadku pozostałych kroków. Ale to się za chwilę zmieni.

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

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

    Ta opcja umożliwia inteligentne dzielenie kodu. Dzięki temu pakiet internetowy wyodrębnia kod dostawcy, jeśli rozmiar przekracza 30 kB (przed minifikacją i kodem gzip). Rozpakowuje też wspólny kod – przydaje się to, gdy kompilacja tworzy kilka pakietów (np. jeśli podzielisz aplikację 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 pobiera wszystkie moduły, które zawierają ścieżki node_modules, i przenosi je do osobnego pliku o nazwie vendor.[chunkhash].js.

Po wprowadzeniu tych zmian każda kompilacja wygeneruje 2 pliki, a nie 1 – main.[chunkhash].js i vendor.[chunkhash].js (vendors~main.[chunkhash].js w pakiecie webpack 4). W przypadku pakietu internetowego 4 pakiet dostawców może nie zostać wygenerowany, jeśli zależności są niewielkie. Nie szkodzi:

$ 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 będzie zapisywać te pliki w pamięci podręcznej oddzielnie i ponownie pobrać tylko zmieniony kod.

Kod środowiska wykonawczego Webpack

Niestety wyodrębnienie samego kodu dostawcy nie wystarczy. Jeśli spróbujesz zmienić coś w kodzie aplikacji:

// index.js
…
…

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

zauważysz, że hasz vendor zmienia się również:

                           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

Wynika to z faktu, że pakiet webpack, oprócz kodu modułów, ma środowisko wykonawcze – krótki fragment kodu, który zarządza realizacją modułu. Gdy podzielisz kod na kilka plików, ten fragment kodu zaczyna się mapować między jego identyfikatorami a odpowiadającymi im plikami:

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

Webpack zawiera to środowisko wykonawcze w ostatnim wygenerowanym fragmencie, czyli w naszym przypadku vendor. Za każdym razem, gdy zmienia się jakiś fragment, ten fragment kodu również się zmienia, co powoduje zmianę całego fragmentu vendor.

Aby rozwiązać ten problem, przenieśmy środowisko wykonawcze do osobnego pliku. W pakiecie internetowym 4 można to zrobić,włączając opcję optimization.runtimeChunk:

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

W pakiecie internetowym 3 możesz to zrobić,tworząc dodatkowy pusty fragment z parametrem 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 wygeneruje 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

Umieść je w kolejności index.html w odwrotnej kolejności. 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 w celu zapisania dodatkowego żądania HTTP

Aby go ulepszyć, spróbuj dodać środowisko wykonawcze pakietu SDK do odpowiedzi HTML. Na przykład zamiast tego:

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

zrób to:

<!-- 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 (jest to dość ważne w przypadku HTTP/1; mniej ważne w przypadku HTTP/2, ale mimo to może powodować).

Oto jak to zrobić.

Jeśli generujesz kod HTML za pomocą htmlWebpackPlugin

Jeśli do generowania pliku HTML używasz wtyczki HtmlWebpackPlugin, potrzebujesz tylko wtyczki 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()
  ]
};

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

W pakiecie webpack 4:

  1. Dodaj 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ą spowoduje utworzenie pliku podobnego do tego:

    // manifest.json
    {
      "runtime~main.js": "runtime~main.8e0d62a03.js"
    }
    
  2. Umieść treść fragmentu środowiska wykonawczego w wygodny sposób. Przykład: w 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>
        …
      `);
    });
    

W pakiecie webpack 3:

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

    module.exports = {
      plugins: [
        new webpack.optimize.CommonsChunkPlugin({
          name: 'runtime',
          minChunks: Infinity,
          filename: 'runtime.js'
        })
      ]
    };
    
  2. Umieść treść runtime.js w treści w wygodny sposób. Przykład: w 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 nie potrzebujesz w tej chwili

Czasami strona składa się z większych, a mniej ważnych części:

  • Gdy wczytujesz w YouTube stronę filmu, bardziej interesuje Cię sam film niż komentarze. W tym przypadku film jest ważniejszy niż komentarze.
  • Jeśli otwierasz artykuł w witrynie z wiadomościami, bardziej interesuje Cię jego treść, a nie reklamy. W tym przypadku tekst jest ważniejszy niż reklamy.

W takich przypadkach możesz przyspieszyć wstępne wczytywanie, pobierając najpierw tylko najważniejsze elementy, a później leniwie ładować pozostałe. Użyj do tego funkcji import() i dzielenia kodu:

// 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 dany moduł ma być ładowany dynamicznie. Gdy pakiet internetowy wykryje atrybut import('./module.js'), przeniesie ten moduł do osobnego fragmentu:

$ 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 ją dopiero wtedy, gdy wykonanie dotrze do funkcji import().

Spowoduje to zmniejszenie pakietu main i skróci czas wczytywania. Co więcej, usprawni to buforowanie – zmiana kodu w głównym fragmencie nie będzie miała wpływu na fragmenty komentarzy.

Więcej informacji

Dzielenie kodu na trasy i strony

Jeśli aplikacja ma wiele tras lub stron, ale zawiera tylko 1 plik JS z kodem (pojedynczy fragment main), prawdopodobnie przy każdym żądaniu udostępnia dodatkowe bajty. Na przykład, gdy użytkownik odwiedzi stronę główną Twojej witryny:

Strona główna WebFundamentals

nie muszą wczytywać kodu, aby wyrenderować artykuł znajdujący się na innej stronie, ale będą to robić. Co więcej, jeśli użytkownik zawsze odwiedza tylko stronę główną, a Ty zmienisz kod artykułu, pakiet internetowy unieważni cały pakiet i użytkownik będzie musiał ponownie pobrać całą aplikację.

Jeśli podzielisz aplikację na strony (lub trasy, jeśli jest to aplikacja jednostronicowa), użytkownik pobierze tylko odpowiedni kod. Dodatkowo przeglądarka będzie lepiej zapisywać kod aplikacji w pamięci podręcznej – jeśli zmienisz kod strony głównej, pakiet internetowy unieważni tylko odpowiedni fragment.

Aplikacje jednostronicowe

Aby podzielić aplikacje jednostronicowe według tras, użyj import() (patrz sekcja „Kod leniwego ładowania, którego obecnie nie potrzebujesz”). Jeśli używasz platformy, może być gotowe rozwiązanie tego problemu:

Tradycyjne aplikacje wielostronicowe

Aby podzielić tradycyjne aplikacje według stron, użyj punktów wejścia w pakiecie webpack. Jeśli aplikacja ma 3 rodzaje stron: stronę główną, stronę z artykułem i stronę konta użytkownika, powinna mieć 3 wpisy:

// 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 Webpack utworzy osobne drzewo zależności i wygeneruje pakiet zawierający tylko moduły używane przez ten wpis:

$ 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 z artykułem korzysta z biblioteki Lodash, pakiety home i profile nie będą jej zawierać, a użytkownik nie będzie musiał pobierać tej biblioteki podczas odwiedzania strony głównej.

Oddzielne drzewa zależności mają jednak swoje wady. Jeśli 2 punkty wejścia korzystają z Lodash i nie przeniesiesz zależności do pakietu dostawców, oba punkty wejścia będą zawierać kopię Lodash. Aby rozwiązać ten problem, w pakiecie webpack 4 dodaj do konfiguracji pakietu internetowego opcję optimization.splitChunks.chunks: 'all':

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

Ta opcja umożliwia inteligentne dzielenie kodu. Dzięki tej opcji pakiet internetowy automatycznie wyszukuje wspólny kod i rozpakowuje go do osobnych plików.

Możesz też użyć pakietu internetowego 3 CommonsChunkPlugin – przeniesie ono typowe zależności do nowego określonego pliku:

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

Wypróbuj wartość minChunks, aby znaleźć najlepszą ofertę. Zasadniczo lepiej jest, jeśli rozmiar będzie niewielki. Jeśli jednak będzie ich więcej, możesz zwiększyć tę liczbę. Na przykład 3 fragmenty mogą mieć minChunks, ale dla 30 może być 8 – jeśli ustawisz tę wartość na 2, zbyt wiele modułów dotrze do wspólnego pliku, przez co będzie on zbyt duży.

Więcej informacji

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

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

$ 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 identyfikator 1 itd.). Problem polega na tym, że gdy dodasz nowy moduł, może on pojawić się na środku listy modułów i zmienić identyfikatory wszystkich kolejnych modułów:

$ 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 nowy moduł...

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

↓ I zobacz, co udało się osiągnąć! Zasób (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

Powoduje to unieważnienie wszystkich fragmentów, które zawierają moduły ze zmienionymi identyfikatorami lub są od nich zależne, nawet jeśli ich rzeczywisty kod nie uległ zmianie. W naszym przypadku fragmenty 0 (z comments.js) i main (fragment z innym kodem aplikacji) zostały unieważnione, a tylko main powinien być unieważniony.

Aby rozwiązać ten problem, zmień sposób obliczania identyfikatorów modułów za pomocą metody HashedModuleIdsPlugin. Zastępuje on identyfikatory oparte na liczniku 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

W tym przypadku identyfikator modułu zmienia się tylko wtedy, gdy zmienisz jego nazwę lub przeniesiesz go. Nowe moduły nie mają wpływu na identyfikatory innych modułów.

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

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

Więcej informacji

Podsumowanie

  • Zapisuj pakiet w pamięci podręcznej i rozróżniaj wersje dzięki zmianie nazwy pakietu
  • Podziel pakiet na kod aplikacji, kod dostawcy i środowisko wykonawcze
  • Wbudowane środowisko wykonawcze do zapisywania żądania HTTP
  • Leniwe ładowanie niekrytycznego kodu za pomocą funkcji import
  • Podziel kod według tras/stron, aby uniknąć wczytywania niepotrzebnych danych