Korzystanie z długoterminowego buforowania

Jak webpack pomaga w przygotowaniu zasobów

Po optymalizacji rozmiaru aplikacji kolejnym sposobem na skrócenie czasu wczytywania aplikacji jest buforowanie. Dzięki temu niektóre części aplikacji będą przechowywane na kliencie, co pozwoli uniknąć ich ponownego pobierania.

Korzystanie z wersji pakietu i nagłówków pamięci podręcznej

Typowe podejście do buforowania polega na:

  1. kazać przeglądarce przechowywać plik w pamięci podręcznej przez bardzo długi czas (np. rok):

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

    Jeśli nie wiesz, do czego służy Cache-Control, przeczytaj świetny artykuł Jake'a Archibalda o sprawdzonych metodach dotyczących pamięci podręcznej.

  2. i zmienić nazwę pliku, aby wymusić jego ponowne pobranie:

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

To podejście informuje przeglądarkę, aby pobierała plik JS, przechowywała go w pamięci podręcznej i używała kopii w pamięci podręcznej. Przeglądarka połączy się z siecią tylko wtedy, gdy zmieni się nazwa pliku (lub minie rok).

W webpacku robisz to samo, ale zamiast numeru wersji podajesz hasz pliku. Aby uwzględnić 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 nazwy pliku, aby wysłać go do klienta, użyj HtmlWebpackPlugin lub WebpackManifestPlugin.

HtmlWebpackPlugin to proste, ale mniej elastyczne podejście. Podczas kompilacji ten wtyczek generuje plik HTML, który zawiera wszystkie skompilowane zasoby. Jeśli logika serwera nie jest skomplikowana, wystarczy:

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

WebpackManifestPluginjest bardziej elastycznym podejściem, które jest przydatne, jeśli masz złożoną część serwera. Podczas kompilacji generuje plik JSON z mapowaniem nazw plików bez hasha i z hashem. Aby dowiedzieć się, z którym plikiem należy pracować, użyj tego pliku JSON na serwerze:

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

Więcej informacji

Wyodrębnianie zależności i czasu wykonywania w osobnym pliku

Zależności

Zależności aplikacji zwykle zmieniają się rzadziej niż sam kod aplikacji. Jeśli przeniesiesz je do osobnego pliku, przeglądarka będzie mogła przechowywać je w oddzielnej pamięci podręcznej i nie będzie ich ponownie pobierać za każdym razem, gdy zmieni się kod aplikacji.

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

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

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

    Gdy webpack kompiluje aplikację, zastępuje [name] nazwą fragmentu. Jeśli nie dodamy części [name], będziemy musieli rozróżniać kawałki po ich haśle, co jest dość trudne.

  2. Przekształć pole entry w 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 nazwą [name] z etapu 1.

    Jeśli w tym momencie skompilujesz aplikację, ten fragment będzie zawierać cały kod aplikacji, tak jakbyśmy nie wykonali tych czynności. Ale to się za chwilę zmieni.

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

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

    Ta opcja umożliwia inteligentne dzielenie kodu. Dzięki niemu webpack wyodrębnia kod dostawcy, jeśli jego rozmiar przekracza 30 kB (przed zminimalizowaniem i gzipowaniem). Wyodrębni też kod wspólny – jest to przydatne, jeśli kompilacja wygeneruje kilka pakietów (np. jeśli podzielisz aplikację na ścieżki).

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

    Ten wtyczka przenosi wszystkie moduły, których ścieżki zawierają node_modules, do osobnego pliku o nazwie vendor.[chunkhash].js.

Po wprowadzeniu tych zmian każda kompilacja będzie generować 2 pliki zamiast 1: main.[chunkhash].jsvendor.[chunkhash].js (vendors~main.[chunkhash].js w przypadku webpacka 4). W przypadku webpacka 4 pakiet dostawcy może nie zostać wygenerowany, jeśli zależności są niewielkie. Nie ma w tym nic złego:

$ 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 przechowuje te pliki w oddzielnej pamięci podręcznej i pobiera ponownie tylko kod, który się zmienił.

Kod środowiska wykonawczego Webpack

Wyodrębnienie samego kodu dostawcy to za mało. Jeśli spróbujesz wprowadzić zmiany w kodzie aplikacji:

// index.js
…
…

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

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

                           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 zawiera czas wykonywania – mały fragment kodu, który zarządza wykonywaniem modułu. Gdy podzielisz kod na kilka plików, ten fragment kodu zacznie zawierać mapowanie identyfikatorów fragmentów i odpowiednich plików:

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

Webpack uwzględnia ten czas wykonywania w ostatnim wygenerowanym fragmencie, który w naszym przypadku to vendor. Za każdym razem, gdy zmienia się jakiś fragment, zmienia się też ten fragment kodu, co powoduje zmianę całego fragmentu vendor.

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

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

W webpack 3 możesz to zrobić,tworząc dodatkowy pusty fragment za pomocą funkcji 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

Umieścić je w index.html w odwrotnej kolejności – to wszystko:

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

Więcej informacji

Wbudowany czas wykonywania webpacka, aby zaoszczędzić dodatkowe żądanie HTTP

Aby jeszcze bardziej zwiększyć wydajność, spróbuj wstawić środowisko wykonawcze webpack do odpowiedzi HTML. Zamiast tego:

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

wykonaj te czynności:

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

Czas wykonywania jest krótki, a wstawienie kodu w źródle pomoże Ci zaoszczędzić żądanie HTTP (co jest bardzo ważne w przypadku HTTP/1, a mniej ważne w przypadku HTTP/2, ale nadal może mieć wpływ).

Oto jak to zrobić.

Jeśli kod HTML jest generowany za pomocą wtyczki HtmlWebpackPlugin

Jeśli do generowania pliku HTML używasz HtmlWebpackPlugin, wystarczy, że użyjesz 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 za pomocą niestandardowej logiki serwera

Z 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 tym wtyczką utworzy plik wyglądający tak:

    // manifest.json
    {
      "runtime~main.js": "runtime~main.8e0d62a03.js"
    }
    
  2. Wstawianie treści fragmentu w czasie wykonywania w wygodny sposób. Przykładowo w przypadku 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 przypadku webpack 3:

  1. Aby ustawić stałą nazwę środowiska wykonawczego, określ parametr filename:

    module.exports = {
      plugins: [
        new webpack.optimize.CommonsChunkPlugin({
          name: 'runtime',
          minChunks: Infinity,
          filename: 'runtime.js'
        })
      ]
    };
    
  2. Wstawiaj treści runtime.js w wygodny sposób. Przykładowo w przypadku 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>
        …
      `);
    });
    

kod wczytywania opóźnionego, którego nie potrzebujesz w tej chwili;

Czasami strona zawiera bardziej i mniej ważne części:

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

W takich przypadkach możesz poprawić początkową wydajność wczytywania, pobierając najpierw tylko najważniejsze elementy, a pozostałe części wczytując z opóźnieniem. Użyj do tego funkcji import()podziału 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 chcesz wczytać określony moduł dynamicznie. Gdy webpack zobaczy 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 go tylko wtedy, gdy wykonanie dotrze do funkcji import().

Dzięki temu pakiet main będzie mniejszy, co skróci czas początkowego wczytywania. Co więcej, poprawi to buforowanie – jeśli zmienisz kod w głównym fragmencie, nie wpłynie to na komentarze.

Więcej informacji

Podziel kod na ścieżki i strony

Jeśli Twoja aplikacja ma wiele tras lub stron, ale zawiera tylko 1 plik JS z kodem (jeden fragment main), prawdopodobnie wysyłasz dodatkowe bajty w każdej prośbie. Gdy na przykład użytkownik odwiedza stronę główną Twojej witryny:

Strona główna WebFundamentals

nie muszą wczytywać kodu do renderowania artykułu na innej stronie – ale go wczytują. Jeśli ponadto użytkownik zawsze odwiedza tylko stronę główną, a Ty wprowadzisz zmianę w kodzie artykułu, webpack unieważni cały pakiet, a użytkownik będzie musiał ponownie pobrać całą aplikację.

Jeśli podzielimy aplikację na strony (lub ścieżki, jeśli jest to aplikacja jednostronicowa), użytkownik pobierze tylko odpowiedni kod. Poza tym przeglądarka będzie lepiej przechowywać w pamięci podręcznej kod aplikacji: jeśli zmienisz kod strony głównej, webpack unieważni tylko odpowiedni fragment.

Aplikacje jednostronicowe

Aby podzielić aplikacje jednostronicowe według ścieżek, użyj import() (patrz sekcja „Kod ładowania opóźnionego, którego nie potrzebujesz obecnie”). Jeśli używasz frameworka, może on zawierać już odpowiednie rozwiązanie:

W przypadku tradycyjnych aplikacji wielostronicowych

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

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

W przypadku każdego pliku wejściowego webpack zbuduje osobne drzewo zależności i wygeneruje pakiet zawierający tylko moduły używane przez ten plik:

$ 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 korzysta z Lodash, pakiety homeprofile nie będą go zawierać, a użytkownik nie będzie musiał go pobierać podczas odwiedzania strony głównej.

Oddzielne drzewa zależności mają jednak pewne wady. Jeśli 2 punkty wejścia używają Lodash, a nie masz zależności przeniesionych do pakietu dostawcy, obie te punkty będą zawierać kopię Lodash. Aby rozwiązać ten problem, w webpack 4 dodaj opcję optimization.splitChunks.chunks: 'all' do konfiguracji webpack:

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

Ta opcja umożliwia inteligentne dzielenie kodu. W przypadku tej opcji webpack automatycznie szukałby wspólnego kodu i wyodrębniał go do osobnych plików.

Możesz też użyć webpacka 3 z opcją CommonsChunkPlugin, aby przenieść typowe zależności do nowego pliku:

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

Możesz zmieniać wartość minChunks, aby znaleźć najlepszą. Zazwyczaj warto zachować mały rozmiar, ale zwiększyć go, jeśli liczba fragmentów rośnie. Na przykład w przypadku 3 fragmentów wartość minChunks może wynosić 2, a w przypadku 30 fragmentów – 8. Jeśli pozostawisz wartość 2, zbyt wiele modułów trafi do wspólnego pliku, co spowoduje jego nadmierne rozszerzanie.

Więcej informacji

Ustabilizowanie identyfikatorów modułów

Podczas kompilowania kodu webpack przypisuje każdemu modułowi identyfikator. Później te identyfikatory są używane w elementach require() w pakiecie. Identyfikatory zwykle pojawiają się w wyniku kompilacji bezpośrednio 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

[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 (np.pierwszy moduł ma identyfikator 0, drugi – identyfikator 1 itd.). Problem polega na tym, że po dodaniu nowego modułu może on pojawić się w środku listy modułów, zmieniając identyfikatory 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 się stało! 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 o zmienionych identyfikatorach lub od nich zależnych, nawet jeśli ich kod nie uległ zmianie. W naszym przypadku nieprawidłowy stał się fragment 0 (z elementem comments.js) oraz fragment main (z innym kodem aplikacji), podczas gdy nieprawidłowy powinien być tylko fragment main.

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

[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 przypadku takiego podejścia 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 identyfikatory innych modułów.

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

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

Więcej informacji

Podsumowanie

  • Zapisz w pamięci podręcznej pakiet i rozróżniaj wersje, zmieniając nazwę pakietu.
  • Podziel pakiet na kod aplikacji, kod dostawcy i środowisko wykonawcze
  • Wstawianie środowiska uruchomieniowego w ciele metody, aby zaoszczędzić żądanie HTTP
  • Lazy-load niekrytycznego kodu za pomocą import
  • Podziel kod według tras/stron, aby uniknąć wczytywania niepotrzebnych elementów