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:
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.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>
WebpackManifestPlugin
jest 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
- Jake Archibald o sprawdzonych metodach dotyczących pamięci podręcznej
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:
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.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.
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 nazwievendor.[chunkhash].js
.
Po wprowadzeniu tych zmian każda kompilacja będzie generować 2 pliki zamiast 1: main.[chunkhash].js
i vendor.[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
- Przewodnik po długotrwałym buforowaniu w Webpacku
- Dokumentacja webpacka dotycząca środowiska wykonawczego i pliku manifestu webpacka
- „Jak najlepiej wykorzystać CommonChunkPlugin”
- Jak działają
optimization.splitChunks
ioptimization.runtimeChunk
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:
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" }
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:
Ustaw statyczną nazwę środowiska wykonawczego, określając
filename
:module.exports = { plugins: [ new webpack.optimize.CommonsChunkPlugin({ name: 'runtime', minChunks: Infinity, filename: 'runtime.js' }) ] };
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()
i 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
- Dokumentacja Webpacka funkcji
import()
- Propozycja kodu JavaScript do implementacji składni
import()
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:
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:
- „Code
Splitting” w dokumentacji
react-router
(dla React) - „Leniwe ładowanie tras” w dokumentach
vue-router
(dla Vue.js)
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 home
i profile
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
- Dokumentacja Webpacka o pojęciu punktów wejścia
- Dokumentacja Webpacka na temat wtyczki CommonsChunkPlugin
- „Jak najlepiej wykorzystać CommonChunkPlugin”
- Jak działają
optimization.splitChunks
ioptimization.runtimeChunk
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
- Dokumentacja Webpacka na temat modułu HashedModuleIdsPlugin
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