Udostępnianie nowoczesnego kodu w nowoczesnych przeglądarkach w celu szybszego wczytywania stron

W tym laboratorium programistycznym możesz zwiększyć 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.

Zrzut ekranu aplikacji

W przykładowej aplikacji możesz wybrać słowo lub emotikon, aby wyrazić, jak bardzo podoba Ci się dany kot. Gdy klikniesz przycisk, aplikacja wyświetli wartość przycisku pod aktualnym obrazem kota.

Zmierz odległość

Zanim zaczniesz optymalizować witrynę, warto najpierw ją sprawdzić:

  1. Aby wyświetlić podgląd strony, kliknij Wyświetl aplikację. Następnie kliknij Pełny ekran pełny ekran.
  2. Aby otworzyć Narzędzia dla programistów, naciśnij `Control+Shift+J` (lub `Command+Option+J` na Macu).
  3. Kliknij kartę Sieć.
  4. Zaznacz pole wyboru Disable cache (Wyłącz pamięć podręczną).
  5. Przeładuj aplikację.

Żądanie dotyczące oryginalnego rozmiaru pakietu

Ta aplikacja zajmuje ponad 80 KB! Czas sprawdzić, czy części pakietu nie są używane:

  1. Naciśnij Control+Shift+P (lub Command+Shift+P na Macu), aby otworzyć menu Command. Menu poleceń

  2. Wpisz Show Coverage i naciśnij Enter, aby wyświetlić kartę Pokrycie.

  3. Na karcie Pokrycie kliknij Odśwież, aby ponownie załadować aplikację podczas rejestrowania pokrycia.

    Załaduj ponownie aplikację z uwzględnieniem pokrycia kodu

  4. Sprawdź, ile kodu zostało użyte i ile zostało załadowane w przypadku głównego pakietu:

    Pokrycie kodu pakietu

Ponad połowa pakietu (44 KB) nie jest nawet wykorzystana. Dzieje się tak, ponieważ wiele elementów kodu składa się z polyfills, aby zapewnić działanie aplikacji w starszych przeglądarkach.

Użyj @babel/preset-env.

Składnia języka JavaScript jest zgodna ze standardem ECMAScript lub ECMA-262. Nowe wersje specyfikacji są publikowane co roku i zawierają nowe funkcje, które przeszły proces zgłaszania propozycji. 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:

Możesz zapoznać się z kodem źródłowym w pliku src/index.js, aby zobaczyć, jak to działa.

Wszystkie te funkcje są obsługiwane w najnowszej wersji Chrome, ale co z innymi przeglądarkami, które ich nie obsługują? Babel, który jest dołączony do aplikacji, to najpopularniejsza biblioteka używana do kompilowania kodu zawierającego nowszą składnię na kod zrozumiały dla starszych przeglądarek i środowisk. Robi to na 2 sposoby:

  • Polyfille są uwzględniane, aby emulować nowsze funkcje ES2015+, dzięki czemu można używać ich interfejsów API, nawet jeśli przeglądarka ich nie obsługuje. Oto przykład polyfilla metody Array.includes.
  • Wtyczki służą do przekształcania kodu ES2015 (lub nowszego) w składnię starszej wersji ES5. Są to zmiany związane z składnią (np. funkcje strzałek), więc nie można ich emulować za pomocą polyfillów.

Aby sprawdzić, które biblioteki Babel są uwzględnione, otwórz plik package.json:

"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. W tym przypadku wszystkie konfiguracje Babel są zdefiniowane w pliku .babelrc w katalogu głównym projektu.
  • babel-loader zawiera Babel w procesie kompilacji webpack.

Teraz spójrz na webpack.config.js, aby zobaczyć, jak babel-loader jest uwzględnione 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, aby mogły działać w środowiskach, które ich nie obsługują. Został już zaimportowany i znajduje się na samej górze src/index.js.
import "./style.css";
import "@babel/polyfill";
  • @babel/preset-env określa, które przekształcenia i polyfille są potrzebne w przypadku przeglądarek lub środowisk wybranych jako cele.

Aby sprawdzić, jak jest on uwzględniony, otwórz plik konfiguracji Babel (.babelrc):

{
  "presets": [
    [
      "@babel/preset-env",
      {
        "targets": "last 2 versions"
      }
    ]
  ]
}

Oto konfiguracja Babel i webpack. Dowiedz się, jak uwzględnić Babel w aplikacji, jeśli używasz innego modułu niż webpack.

Atrybut targets w sekcji .babelrc wskazuje, które przeglądarki są kierowane. @babel/preset-envintegruje się z listą browserslist, co oznacza, że w dokumentacji browserslist znajdziesz pełną listę zgodnych zapytań, które można używać w tym polu.

Wartość "last 2 versions" przetłumaczy kod w aplikacji na dwie ostatnie wersje każdej przeglądarki.

Debugowanie

Aby uzyskać pełny przegląd wszystkich celów Babel przeglądarki oraz wszystkich zawartych w nim przekształceń i polyfilli, 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 na dole edytora.

Przeglądarki docelowe

Babel rejestruje w konsoli wiele szczegółów dotyczących procesu kompilacji, w tym wszystkie środowiska docelowe, dla których został skompilowany kod.

Przeglądarki docelowe

Zwróć uwagę, że na tej liście znajdują się przeglądarki wycofane, takie jak Internet Explorer. To problem, ponieważ w nieobsługiwanych przeglądarkach nie będą dostępne nowsze funkcje, a Babel będzie nadal transpilować dla nich określoną składnię. To niepotrzebnie zwiększa rozmiar pakietu, jeśli użytkownicy nie korzystają z tego przeglądarki, aby uzyskać dostęp do Twojej witryny.

Babel rejestruje też listę użytych wtyczek transformacji:

Lista używanych wtyczek

To dość długa lista. To wszystkie wtyczki, których Babel musi używać do przekształcania dowolnej składni ES2015+ w starszą składnię we wszystkich docelowych przeglądarkach.

Babel nie pokazuje jednak konkretnych polyfillów, które są używane:

Nie dodano żadnych funkcji zastępczych

Dzieje się tak, ponieważ cały plik @babel/polyfill jest importowany bezpośrednio.

Ładowanie polyfilli pojedynczo

Domyślnie Babel zawiera wszystkie polyfille potrzebne do stworzenia pełnego środowiska ES2015+, gdy @babel/polyfill jest importowany do pliku. Aby zaimportować konkretne polyfille wymagane w przypadku docelowych przeglądarek, dodaj do konfiguracji useBuiltIns: 'entry'.

{
  "presets": [
    [
      "@babel/preset-env",
      {
        "targets": "last 2 versions",
        "debug": true
        "useBuiltIns": "entry"
      }
    ]
  ]
}

Załaduj ponownie aplikację. Możesz teraz zobaczyć wszystkie uwzględnione polyfille:

Lista zaimportowanych polyfilli

Chociaż uwzględniliśmy tylko potrzebne rozwiązania polyfill dla "last 2 versions", lista jest nadal bardzo długa. Dzieje się tak, ponieważ nadal są uwzględniane polyfille potrzebne w przypadku przeglądarek docelowych dla każdej nowszej funkcji. Zmień wartość atrybutu na usage, aby uwzględnić tylko te, które są potrzebne do obsługi 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 polyfille są dołączane automatycznie. Oznacza to, że możesz usunąć import @babel/polyfill w sekcji src/index.js..

import "./style.css";
import "@babel/polyfill";

Teraz uwzględniane są tylko wymagane polyfille potrzebne do aplikacji.

Lista automatycznie dołączonych polyfilli

Rozmiar pakietu aplikacji jest znacznie mniejszy.

Rozmiar pakietu został zmniejszony do 30,1 KB

Zawężenie listy obsługiwanych przeglądarek

Liczba uwzględnionych docelowych przeglądarek jest nadal dość duża, a niewielu użytkowników korzysta z przeglądarek wycofanych z obsługi, takich jak Internet Explorer. Zaktualizuj konfiguracje, aby wyglądały tak:

{
  "presets": [
    [
      "@babel/preset-env",
      {
        "targets": "last 2 versions",
        "targets": [">0.25%", "not ie 11"],
        "debug": true,
        "useBuiltIns": "usage",
      }
    ]
  ]
}

Sprawdź szczegóły pobranego pakietu.

Rozmiar pakietu: 30,0 KB

Aplikacja jest bardzo mała, więc te zmiany nie mają większego znaczenia. Zalecamy jednak stosowanie procentowego udziału przeglądarki w rynku (np. ">0.25%") oraz wykluczanie konkretnych przeglądarek, których Twoi użytkownicy na pewno nie używają. Aby dowiedzieć się więcej, przeczytaj artykuł „Ostatnie 2 wersje uznane za szkodliwe” autorstwa Jamesa Kyle’a.

Użyj tagu <script type="module">

Nadal jest miejsce na poprawę. Chociaż usunięto wiele nieużywanych polyfillów, wiele z nich jest nadal dostarczanych, ponieważ nie jest potrzebne w niektórych przeglądarkach. Dzięki modułom nowszą składnię można pisać i przesyłać bezpośrednio do przeglądarek bez używania zbędnych polyfilli.

Moduły JavaScript to stosunkowo nowa funkcja obsługiwana w wszystkich głównych przeglądarkach. Aby zdefiniować skrypty, które mają być importowane i eksportowane z innych modułów, możesz utworzyć moduły za pomocą atrybutu type="module". 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 JavaScript (bez konieczności korzystania z Babel). Oznacza to, że plik konfiguracji Babel można zmodyfikować, aby wysyłać do przeglądarki 2 różne wersje aplikacji:

  • Wersja, która działa w nowszych przeglądarkach obsługujących moduły i zawiera moduł, który jest w większości nieprzetłumaczone, ale ma mniejszy rozmiar pliku.
  • wersja, która zawiera większy skompilowany skrypt, który działa w dowolnej starszej przeglądarce;

Korzystanie z modułów ES w Babelu

Aby mieć osobne ustawienia @babel/preset-env dla 2 wersji aplikacji, usuń plik .babelrc. Ustawienia Babel można dodać do konfiguracji webpack, podając 2 różne formaty kompilacji dla każdej wersji aplikacji.

Zacznij od dodania konfiguracji starszego skryptu do pliku 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 elementu "@babel/preset-env" używana jest wartość esmodules z wartością false. Oznacza to, że Babel zawiera wszystkie niezbędne przekształcenia i polyfille, aby kierować na 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 dane są współdzielone przez moduł i starszą wersję skryptów wysyłanych do przeglądarki.

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"})
];

Teraz w podobny sposób utwórz obiekt konfiguracji dla skryptu modułu poniżej, w którym zdefiniowano zmienną 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 nazwie pliku wyjściowego jest używane rozszerzenie .mjs. Wartość esmodules jest tutaj ustawiona na „PRAWDA”, co oznacza, że kod generowany w tym module jest krótszy i mniej skompilowany, a w tym przykładzie nie przechodzi przez żadną transformację, ponieważ wszystkie używane funkcje są już obsługiwane w przeglądarkach obsługujących moduły.

Pod koniec 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 skompilowany skrypt dla starszych przeglądarek.

Przeglądarki, które obsługują moduły, ignorują skrypty z atrybutem nomodule. Z kolei przeglądarki, które nie obsługują modułów, ignorują elementy skryptu z wartością type="module". Oznacza to, że możesz dołączyć moduł i skompilowany plik zastępczy. W idealnej sytuacji obie wersje aplikacji powinny być w formacie index.html, na przykład:

<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.

Pamiętaj, że w odróżnieniu od zwykłych skryptów skrypty modułów są zawsze domyślnie opóźnione. Jeśli chcesz, aby skrypt nomodule był również opóźniony i wykonany dopiero po przeanalizowaniu, musisz dodać atrybut defer:

<script type="module" src="main.mjs"></script>
<script nomodule src="main.bundle.js" defer></script>

Ostatnią rzeczą, którą musisz zrobić, jest dodanie atrybutów module i nomodule odpowiednio do modułu i starszego skryptu. Zaimportuj ScriptExtHtmlWebpackPlugin na samym szczycie 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 skryptu .mjs, a także atrybut nomodule do wszystkich modułów skryptu .js.

Wyświetlanie modułów w dokumencie HTML

Ostatnią rzeczą, którą trzeba zrobić, jest wyprowadzenie do pliku HTML zarówno starszych, jak i nowych elementów skryptu. Niestety wtyczka, która tworzy finalny plik HTML (HTMLWebpackPlugin), nie obsługuje obecnie wyjścia skryptów module i nomodule. Chociaż istnieją obejścia i osobne wtyczki stworzone w celu rozwiązania tego problemu, takie jak BabelMultiTargetPluginHTMLWebpackMultiBuildPlugin, w tym samouczku zastosowano prostsze podejście polegające na ręcznym dodawaniu elementu skryptu modułu.

Dodaj do pliku src/index.js te informacje:

    ...
    </form>
    <script type="module" src="main.mjs"></script>
  </body>
</html>

Teraz otwórz aplikację w przeglądarce obsługującej moduły, np. w najnowszej wersji Chrome.

Moduł o rozmaju 5,2 KB pobierany przez sieć w przypadku nowszych przeglądarek

Pobierany jest tylko moduł, który ma znacznie mniejszy rozmiar, ponieważ nie jest w dużej mierze przetłumaczony. Inny element skryptu jest całkowicie ignorowany przez przeglądarkę.

Jeśli ładujesz aplikację w starszej przeglądarce, pobierany jest tylko większy skompilowany skrypt ze wszystkimi niezbędnymi polyfillami i transformacjami. Oto zrzut ekranu ze wszystkimi żądaniami wysłanymi w starszej wersji Chrome (38).

Skrypt o rozmiarze 30 KB pobrany na potrzeby starszych przeglądarek

Podsumowanie

Teraz wiesz, jak używać @babel/preset-env, aby udostępniać tylko te polyfille, które są wymagane w przypadku wybranych przeglądarek. Wiesz też, że moduły JavaScript mogą zwiększyć wydajność, przesyłając 2 różne przetłumaczone wersje aplikacji. Teraz, gdy już wiesz, jak te 2 techniki mogą znacznie zmniejszyć rozmiar pakietu, możesz przystąpić do optymalizacji.