W tym laboratorium kodowania poprawisz wydajność prostej aplikacji, która umożliwia użytkownikom ocenianie losowych kotów. Dowiedz się, jak zoptymalizować pakiet JavaScript, minimalizując ilość kodu poddawanego transpilacji.
W przykładowej aplikacji możesz wybrać słowo lub emoji, aby wyrazić, jak bardzo podoba Ci się dany kot. Gdy klikniesz przycisk, aplikacja wyświetli jego wartość pod aktualnym zdjęciem kota.
Pomiary
Zanim zaczniesz wprowadzać optymalizacje, warto najpierw sprawdzić witrynę:
- Aby wyświetlić podgląd strony, kliknij Wyświetl aplikację, a następnie Pełny ekran
.
- Aby otworzyć Narzędzia dla programistów, naciśnij Ctrl+Shift+J (lub Command+Option+J na Macu).
- Kliknij kartę Sieć.
- Zaznacz pole wyboru Wyłącz pamięć podręczną.
- Ponownie załaduj aplikację.
Ta aplikacja zajmuje ponad 80 KB. Sprawdź, czy niektóre części pakietu nie są używane:
Naciśnij
Control+Shift+P
(lubCommand+Shift+P
na Macu), aby otworzyć menu Polecenie.Wpisz
Show Coverage
i naciśnijEnter
, aby wyświetlić kartę Pokrycie.Na karcie Pokrycie kliknij Załaduj ponownie, aby ponownie załadować aplikację podczas rejestrowania pokrycia.
Sprawdź, ile kodu zostało użyte w porównaniu z ilością kodu wczytanego w głównym pakiecie:
Ponad połowa pakietu (44 KB) nie jest nawet wykorzystywana. Dzieje się tak, ponieważ wiele fragmentów kodu zawiera polyfille, które zapewniają działanie aplikacji w starszych przeglądarkach.
Używanie @babel/preset-env
Składnia języka JavaScript jest zgodna ze standardem ECMAScript, czyli ECMA-262. Nowsze wersje specyfikacji są publikowane co roku i zawierają nowe funkcje, które przeszły proces proponowania. Każda z głównych przeglądarek jest na innym etapie obsługi tych funkcji.
W aplikacji używane są te funkcje ES2015:
Używana jest też ta funkcja ES2017:
Zapoznaj się z kodem źródłowym w src/index.js
, aby zobaczyć, jak to wszystko jest używane.
Wszystkie te funkcje są obsługiwane w najnowszej wersji Chrome, ale co z innymi przeglądarkami, które ich nie obsługują? Babel, która jest częścią aplikacji, to najpopularniejsza biblioteka używana do kompilowania kodu zawierającego nowszą składnię w kod, który mogą odczytać starsze przeglądarki i środowiska. Można to zrobić na 2 sposoby:
- Polyfills są dołączane w celu emulowania nowszych funkcji ES2015+, dzięki czemu można używać ich interfejsów API, nawet jeśli nie są obsługiwane przez przeglądarkę. Oto przykład polyfillu metody
Array.includes
. - Wtyczki służą do przekształcania kodu ES2015 (lub nowszego) w starszą składnię ES5. Są to zmiany związane ze składnią (np. funkcje strzałkowe), więc nie można ich emulować za pomocą polyfilli.
Sprawdź package.json
, aby zobaczyć, które biblioteki Babel są uwzględnione:
"dependencies": {
"@babel/polyfill": "^7.0.0"
},
"devDependencies": {
//...
"babel-loader": "^8.0.2",
"@babel/core": "^7.1.0",
"@babel/preset-env": "^7.1.0",
//...
}
@babel/core
to podstawowy kompilator Babel. Dzięki temu wszystkie konfiguracje Babel są zdefiniowane w pliku.babelrc
w katalogu głównym projektu.babel-loader
uwzględnia Babel w procesie kompilacji webpack.
Teraz spójrz na webpack.config.js
, aby zobaczyć, jak babel-loader
jest uwzględniony jako reguła:
module: { rules: [ //... { test: /\.js$/, exclude: /node_modules/, loader: "babel-loader" } ] },
@babel/polyfill
udostępnia wszystkie niezbędne polyfille dla nowszych funkcji ECMAScript, dzięki czemu mogą one działać w środowiskach, które ich nie obsługują. Jest już zaimportowany na samym początkusrc/index.js.
import "./style.css";
import "@babel/polyfill";
@babel/preset-env
określa, które przekształcenia i wypełnienia są niezbędne w przypadku przeglądarek lub środowisk wybranych jako cele.
Sprawdź plik konfiguracji Babel .babelrc
, aby zobaczyć, jak jest on uwzględniany:
{
"presets": [
[
"@babel/preset-env",
{
"targets": "last 2 versions"
}
]
]
}
Jest to konfiguracja Babel i webpack. Dowiedz się, jak uwzględnić Babel w aplikacji, jeśli używasz innego narzędzia do łączenia modułów niż webpack.
Atrybut targets
w .babelrc
określa przeglądarki, na które kierowane są reklamy. @babel/preset-env
jest zintegrowany z browserslist, co oznacza, że pełną listę zgodnych zapytań, których można używać w tym polu, znajdziesz w dokumentacji browserslist.
Wartość "last 2 versions"
przekształca kod w aplikacji na potrzeby 2 ostatnich wersji każdej przeglądarki.
Debugowanie
Aby uzyskać pełny wgląd we wszystkie cele Babel przeglądarki, a także wszystkie transformacje i polyfille, które są uwzględnione, dodaj pole debug
do .babelrc:
{
"presets": [
[
"@babel/preset-env",
{
"targets": "last 2 versions",
"debug": true
}
]
]
}
- Kliknij Narzędzia.
- Kliknij Logi.
Ponownie załaduj aplikację i sprawdź logi stanu Glitch u dołu edytora.
Przeglądarki, na które można kierować reklamy
Babel rejestruje w konsoli wiele szczegółów dotyczących procesu kompilacji, w tym wszystkie środowiska docelowe, dla których skompilowano kod.
Zwróć uwagę, że na tej liście znajdują się wycofane przeglądarki, takie jak Internet Explorer. To problem, ponieważ nieobsługiwane przeglądarki nie będą miały dodawanych nowszych funkcji, a Babel nadal będzie dla nich transpilować określoną składnię. Jeśli użytkownicy nie korzystają z tej przeglądarki, aby uzyskać dostęp do Twojej witryny, niepotrzebnie zwiększa to rozmiar pakietu.
Babel rejestruje też listę używanych wtyczek transformacji:
To dość długa lista. Są to wszystkie wtyczki, których Babel potrzebuje do przekształcenia składni ES2015+ na starszą składnię dla wszystkich docelowych przeglądarek.
Babel nie wyświetla jednak żadnych konkretnych używanych polyfilli:
Dzieje się tak, ponieważ cały plik @babel/polyfill
jest importowany bezpośrednio.
Wczytywanie pojedynczych polyfilli
Domyślnie Babel zawiera wszystkie polyfille potrzebne do pełnego środowiska ES2015+, gdy @babel/polyfill
jest importowany do pliku. Aby zaimportować konkretne polyfille potrzebne w przypadku przeglądarek docelowych, dodaj do konfiguracji znak useBuiltIns: 'entry'
.
{
"presets": [
[
"@babel/preset-env",
{
"targets": "last 2 versions",
"debug": true
"useBuiltIns": "entry"
}
]
]
}
Załaduj ponownie aplikację. Teraz możesz zobaczyć wszystkie uwzględnione polyfille:
Obecnie uwzględniane są tylko potrzebne polyfille dla "last 2 versions"
, ale lista nadal jest bardzo długa. Dzieje się tak, ponieważ polifille potrzebne w przypadku przeglądarek docelowych dla każdej nowszej funkcji są nadal uwzględniane. Zmień wartość atrybutu na usage
, aby uwzględnić tylko te atrybuty, które są potrzebne w przypadku funkcji używanych w kodzie.
{
"presets": [
[
"@babel/preset-env",
{
"targets": "last 2 versions",
"debug": true,
"useBuiltIns": "entry"
"useBuiltIns": "usage"
}
]
]
}
Dzięki temu w razie potrzeby automatycznie uwzględniane są polyfille.
Oznacza to, że możesz usunąć import @babel/polyfill
w src/index.js.
.
import "./style.css";
import "@babel/polyfill";
Teraz uwzględniane są tylko wymagane polyfille potrzebne w aplikacji.
Rozmiar pakietu aplikacji jest znacznie mniejszy.
Ograniczanie listy obsługiwanych przeglądarek
Liczba uwzględnionych przeglądarek jest nadal dość duża, a niewielu użytkowników korzysta z wycofanych przeglądarek, takich jak Internet Explorer. Zaktualizuj konfiguracje w ten sposób:
{
"presets": [
[
"@babel/preset-env",
{
"targets": "last 2 versions",
"targets": [">0.25%", "not ie 11"],
"debug": true,
"useBuiltIns": "usage",
}
]
]
}
Sprawdź szczegóły pobranego pakietu.
Ponieważ aplikacja jest bardzo mała, te zmiany nie robią dużej różnicy. Zalecamy jednak używanie procentowego udziału przeglądarki w rynku (np.">0.25%"
) wraz z wykluczaniem konkretnych przeglądarek, których Twoi użytkownicy na pewno nie używają. Więcej informacji znajdziesz w artykule „Last 2 versions” considered harmful (Ostatnie 2 wersje uważane za szkodliwe) autorstwa Jamesa Kyle’a.
Użyj tagu <script type="module">
Można go jeszcze ulepszyć. Usunęliśmy kilka nieużywanych polyfilli, ale nadal wysyłamy wiele takich elementów, które nie są potrzebne w niektórych przeglądarkach. Dzięki modułom można pisać nowszą składnię i wysyłać ją bezpośrednio do przeglądarek bez używania niepotrzebnych polyfilli.
Moduły JavaScript to stosunkowo nowa funkcja obsługiwana przez wszystkie główne przeglądarki.
Moduły można tworzyć za pomocą atrybutu type="module"
, aby definiować skrypty, które importują i eksportują z innych modułów. Na przykład:
// math.mjs
export const add = (x, y) => x + y;
<!-- index.html -->
<script type="module">
import { add } from './math.mjs';
add(5, 2); // 7
</script>
Wiele nowszych funkcji ECMAScript jest już obsługiwanych w środowiskach, które obsługują moduły JavaScriptu (zamiast wymagać Babel). Oznacza to, że konfigurację Babel można zmodyfikować tak, aby do przeglądarki wysyłać 2 różne wersje aplikacji:
- wersję, która będzie działać w nowszych przeglądarkach obsługujących moduły i zawiera moduł, który w dużej mierze nie został przetranspilowany, ale ma mniejszy rozmiar pliku;
- wersję zawierającą większy, skompilowany skrypt, który działa w każdej starszej przeglądarce;
Używanie modułów ES z Babel
Aby mieć osobne ustawienia @babel/preset-env
dla 2 wersji aplikacji, usuń plik .babelrc
. Ustawienia Babel można dodać do konfiguracji webpacka, określając 2 różne formaty kompilacji dla każdej wersji aplikacji.
Zacznij od dodania konfiguracji starszego skryptu do webpack.config.js
:
const legacyConfig = {
entry,
output: {
path: path.resolve(__dirname, "public"),
filename: "[name].bundle.js"
},
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
loader: "babel-loader",
options: {
presets: [
["@babel/preset-env", {
useBuiltIns: "usage",
targets: {
esmodules: false
}
}]
]
}
},
cssRule
]
},
plugins
}
Zwróć uwagę, że zamiast wartości targets
dla "@babel/preset-env"
,
esmodules
używana jest wartość false
. Oznacza to, że Babel zawiera wszystkie niezbędne przekształcenia i wypełnienia, aby obsługiwać każdą przeglądarkę, która nie obsługuje jeszcze modułów ES.
Dodaj obiekty entry
, cssRule
i corePlugins
na początku pliku webpack.config.js
. Wszystkie te elementy są współdzielone między modułem a starszymi skryptami wyświetlanymi w przeglądarce.
const entry = {
main: "./src"
};
const cssRule = {
test: /\.css$/,
use: ExtractTextPlugin.extract({
fallback: "style-loader",
use: "css-loader"
})
};
const plugins = [
new ExtractTextPlugin({filename: "[name].css", allChunks: true}),
new HtmlWebpackPlugin({template: "./src/index.html"})
];
Podobnie utwórz obiekt konfiguracji dla skryptu modułu poniżej, w którym zdefiniowano legacyConfig
:
const moduleConfig = {
entry,
output: {
path: path.resolve(__dirname, "public"),
filename: "[name].mjs"
},
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
loader: "babel-loader",
options: {
presets: [
["@babel/preset-env", {
useBuiltIns: "usage",
targets: {
esmodules: true
}
}]
]
}
},
cssRule
]
},
plugins
}
Główna różnica polega na tym, że w przypadku nazwy pliku wyjściowego używane jest rozszerzenie .mjs
. Wartość esmodules
jest tutaj ustawiona na „true”, co oznacza, że kod wyjściowy w tym module to mniejszy, mniej skompilowany skrypt, który w tym przykładzie nie przechodzi żadnej transformacji, ponieważ wszystkie użyte funkcje są już obsługiwane w przeglądarkach, które obsługują moduły.
Na końcu pliku wyeksportuj obie konfiguracje w jednej tablicy.
module.exports = [
legacyConfig, moduleConfig
];
Teraz tworzy on mniejszy moduł dla przeglądarek, które go obsługują, oraz większy skrypt po transpilacji dla starszych przeglądarek.
Przeglądarki obsługujące moduły ignorują skrypty z atrybutem nomodule
.
Z kolei przeglądarki, które nie obsługują modułów, ignorują elementy skryptu z atrybutem type="module"
. Oznacza to, że możesz uwzględnić moduł, a także skompilowaną wartość zastępczą. Najlepiej, aby obie wersje aplikacji były w index.html
, np.:
<script type="module" src="main.mjs"></script>
<script nomodule src="main.bundle.js"></script>
Przeglądarki, które obsługują moduły, pobierają i wykonują main.mjs
, a ignorują main.bundle.js.
. Przeglądarki, które nie obsługują modułów, robią odwrotnie.
Warto pamiętać, że w przeciwieństwie do zwykłych skryptów skrypty modułów są domyślnie zawsze odroczone.
Jeśli chcesz, aby równoważny skrypt nomodule
również był odroczony i wykonywany dopiero po przeanalizowaniu, musisz dodać atrybut defer
:
<script type="module" src="main.mjs"></script>
<script nomodule src="main.bundle.js" defer></script>
Ostatnią rzeczą, jaką musisz zrobić, jest dodanie atrybutów module
i nomodule
do modułu i starszego skryptu. Zaimportuj ScriptExtHtmlWebpackPlugin na samym początku pliku webpack.config.js
:
const path = require("path");
const webpack = require("webpack");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const ScriptExtHtmlWebpackPlugin = require("script-ext-html-webpack-plugin");
Teraz zaktualizuj tablicę plugins
w konfiguracjach, aby uwzględnić tę wtyczkę:
const plugins = [ new ExtractTextPlugin({filename: "[name].css", allChunks: true}), new HtmlWebpackPlugin({template: "./src/index.html"}), new ScriptExtHtmlWebpackPlugin({ module: /\.mjs$/, custom: [ { test: /\.js$/, attribute: 'nomodule', value: '' }, ] }) ];
Te ustawienia wtyczki dodają atrybut type="module"
do wszystkich elementów .mjs
script oraz atrybut nomodule
do wszystkich modułów skryptu .js
.
Wyświetlanie modułów w dokumencie HTML
Ostatnią rzeczą, jaką musisz zrobić, jest wygenerowanie w pliku HTML elementów skryptu starszego i nowoczesnego. Wtyczka, która tworzy końcowy plik HTML, HTMLWebpackPlugin
, nie obsługuje obecnie danych wyjściowych skryptów modułu i skryptów nomodule. Chociaż istnieją obejścia i osobne wtyczki, które rozwiązują ten problem, np. BabelMultiTargetPlugin i HTMLWebpackMultiBuildPlugin, na potrzeby tego samouczka używamy prostszego podejścia polegającego na ręcznym dodaniu elementu skryptu modułu.
Na końcu pliku src/index.js
dodaj te wiersze:
...
</form>
<script type="module" src="main.mjs"></script>
</body>
</html>
Teraz załaduj aplikację w przeglądarce obsługującej moduły, np. w najnowszej wersji Chrome.
Pobierany jest tylko moduł, a rozmiar pakietu jest znacznie mniejszy, ponieważ w dużej mierze nie jest on transpilowany. Drugi element skryptu jest całkowicie ignorowany przez przeglądarkę.
Jeśli wczytasz aplikację w starszej przeglądarce, pobrany zostanie tylko większy, skompilowany skrypt ze wszystkimi potrzebnymi polyfillami i transformacjami. Oto zrzut ekranu przedstawiający wszystkie żądania wysłane w starszej wersji Chrome (wersja 38).
Podsumowanie
Wiesz już, jak używać @babel/preset-env
, aby udostępniać tylko niezbędne polyfille wymagane w przypadku docelowych przeglądarek. Wiesz też, jak moduły JavaScript mogą jeszcze bardziej zwiększyć wydajność, dostarczając 2 różne przetranspilowane wersje aplikacji. Dzięki dobremu zrozumieniu, jak obie te techniki mogą znacznie zmniejszyć rozmiar pakietu, możesz zacząć optymalizację.