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:
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.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
- Jake Archibald o sprawdzonych metodach dotyczących buforowania
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:
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.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.
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 nazwievendor.[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
- Przewodnik po pakiecie internetowym związane z długoterminowym zapisywaniem w pamięci podręcznej
- Dokumentacja pakietu Webpack dotyczącego środowiska wykonawczego i pliku manifestu
- „Wykorzystywanie pełnego potencjału CommonsChunkPlugin”
- Jak działają
optimization.splitChunks
ioptimization.runtimeChunk
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:
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" }
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:
Ustaw nazwę środowiska wykonawczego jako statyczną, określając
filename
:module.exports = { plugins: [ new webpack.optimize.CommonsChunkPlugin({ name: 'runtime', minChunks: Infinity, filename: 'runtime.js' }) ] };
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
- Dokumentacja pakietu internetowego dla funkcji
import()
- Propozycja wdrożenia składni
import()
w języku JavaScript
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:
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:
- „Code
Splitting”
w dokumentach
react-router
(dla React) - „Leniwe ładowanie tras” w dokumentacji
vue-router
(dla Vue.js)
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
- Dokumentacja pakietu internetowego na temat punktów wejścia
- Dokumentacja pakietu Webpack o CommonsChunkPlugin
- „Wykorzystywanie pełnego potencjału CommonsChunkPlugin”
- 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 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
- Dokumentacja pakietu internetowego dotycząca wtyczki HashedModuleIdsPlugin
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