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. poinstruuj przeglądarkę, aby zapisywała plik w pamięci podręcznej przez bardzo długi czas (np. rok):

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

    Jeśli nie wiesz, co robi Cache-Control, przeczytaj świetny post Jake'a Archibalda na temat sprawdzonych metod buforowania.

  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 łączy się z siecią tylko po zmianie nazwy pliku (lub po upływie roku).

W przypadku webpacka 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 generowany jest plik JSON z mapowaniem nazw plików bez haszy i nazw plików z haszowaniem. 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 na podstawie ich hasha, 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 temu webpack wyodrębnia kod dostawcy, jeśli jego rozmiar przekracza 30 kB (przed kompresją i gzipowaniem). Wyodrębniłby też wspólny kod – przydaje się to, gdy kompilacja generuje kilka pakietów (np. jeśli aplikacja została podzielona na trasy).

    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 pakietu Webpack 4 pakiet dostawcy 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 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 dołącza to środowisko wykonawcze do ostatniego wygenerowanego fragmentu, czyli w naszym przypadku vendor. Za każdym razem, gdy zmienia się jakikolwiek fragment kodu, zmienia się też ten fragment kodu, przez co cały fragment vendor ulega zmianie.

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

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

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. To znaczy, że:

<!-- 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 wtykiem utworzy plik o takiej strukturze:

    // manifest.json
    {
      "runtime~main.js": "runtime~main.8e0d62a03.js"
    }
    
  2. Możesz w wygodny sposób umieścić treść fragmentu środowiska wykonawczego. 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 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. 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 zależy Ci na jego treści niż na reklamach. 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 celu funkcji import()podzielonego 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 dyrektywę import('./module.js'), przenosi 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 przypadku każdego żądania. Gdy na przykład użytkownik odwiedza stronę główną Twojej witryny:

Strona główna WebFundamentals

nie muszą wczytywać kodu wyrenderowanego artykułu, który znajduje się na innej stronie, ale wczytają go. 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. Co więcej, 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 trasy, użyj funkcji import() (zobacz sekcję „Leniwe ładowanie kodu, którego teraz nie potrzebujesz”). Jeśli używasz frameworka, może on zawierać już 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ę z artykułem i stronę konta użytkownika, powinna zawierać 3 pozycje:

// 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ł pobierać tej biblioteki podczas odwiedzania strony głównej.

Oddzielne drzewa zależności mają jednak pewne 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 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. Gdy ta opcja jest włączona, Webpack automatycznie szuka wspólnego kodu i wyodrębnia go do osobnych plików.

Możesz też w pakiecie webpack 3 użyć polecenia CommonsChunkPlugin – spowoduje to przeniesienie wspólnych 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ę. 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

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 są widoczne 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

[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 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 zależne od nich, nawet jeśli ich rzeczywisty kod się nie zmienił. W naszym przypadku fragmenty 0 (z comments.js) i main (ten fragment z drugim kodem aplikacji) są unieważnione, a powinna być tylko 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 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
  • Wstawianie środowiska uruchomieniowego w ciele, aby zaoszczędzić żądanie HTTP
  • Lazy-load niekrytycznego kodu za pomocą import
  • Podziel kod według tras/stron, aby uniknąć wczytywania niepotrzebnych elementów