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

Dzięki tym ćwiczeniom w Codelabs poprawisz wydajność tej prostej aplikacji, która umożliwia użytkownikom ocenianie przypadkowych kotów. Dowiedz się, jak zoptymalizować pakiet JavaScript przez minimalizację ilości kodu transpilowanego.

Zrzut ekranu aplikacji

W przykładowej aplikacji możesz wybrać słowo lub emotikon, by pokazać, jak bardzo podoba Ci się każdy z kotów. Gdy klikniesz przycisk, aplikacja wyświetli jego wartość pod bieżącym zdjęciem kota.

Zmierz odległość

Zanim wprowadzisz jakiekolwiek optymalizacje, warto zacząć od sprawdzenia witryny:

  1. Aby wyświetlić podgląd witryny, kliknij Wyświetl aplikację, a następnie naciśnij Pełny ekran pełny ekran.
  2. Naciśnij „Control + Shift + J” (lub „Command + Option + J” na Macu), aby otworzyć Narzędzia deweloperskie.
  3. Kliknij kartę Sieć.
  4. Zaznacz pole wyboru Wyłącz pamięć podręczną.
  5. Załaduj ponownie aplikację.

Żądanie dotyczące pierwotnego rozmiaru pakietu

Ta aplikacja zajmuje ponad 80 KB! Czas sprawdzić, czy elementy 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ę Stan.

  3. Na karcie Pokrycie kliknij Załaduj ponownie, aby ponownie załadować aplikację i sprawdzić pokrycie.

    Załaduj ponownie aplikację z zasięgiem kodu

  4. Przyjrzyj się wykorzystaniu kodu w porównaniu z ilością wczytanego kodu w przypadku pakietu głównego:

    Zasięg kodu pakietu

Ponad połowa pakietu (44 KB) nie jest nawet używana. Dzieje się tak, ponieważ znaczna część kodu składa się z elementów polyfill, dzięki czemu aplikacja działa w starszych przeglądarkach.

Użyj parametru @babel/preset-env

Składnia języka JavaScript jest zgodna ze standardem znanym jako ECMAScript lub ECMA-262. Co roku publikowane są nowsze wersje specyfikacji i obejmują nowe funkcje, które nie przeszły procesu składania oferty. Każda z najpopularniejszych przeglądarek jest zawsze na innym etapie obsługi tych funkcji.

W aplikacji używane są te funkcje zgodne ze standardem ES2015:

Używana jest też ta funkcja ES2017:

Zapoznaj się z kodem źródłowym w języku src/index.js, by zobaczyć, jak wszystkie te funkcje są wykorzystywane.

Wszystkie wymienione funkcje są dostępne w najnowszej wersji Chrome, ale co z innymi przeglądarkami, które ich nie obsługują? Biblioteka Babel, która wchodzi w skład aplikacji, to najpopularniejsza biblioteka używana do kompilowania kodu zawierającego nowszą składnię, aby kod był zrozumiały dla starszych przeglądarek i środowisk. Odbywa się to na 2 sposoby:

  • Elementy polyfill są uwzględniane, aby emulować nowsze funkcje ES2015+, dzięki czemu ich interfejsy API mogą być używane, nawet jeśli nie są obsługiwane przez przeglądarkę. Oto przykład kodu polyfill metody Array.includes.
  • Wtyczki służą do przekształcania kodu ES2015 (lub nowszego) na starszą składnię ES5. Są to zmiany związane ze składnią (np. funkcje strzałek), więc nie można ich emulować za pomocą elementów polyfill.

Otwórz package.json, aby sprawdzić, 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 elemencie głównym projektu (.babelrc).
  • babel-loader włącza Babel w proces kompilacji pakietu internetowego.

Teraz spójrz na obiekt webpack.config.js, by zobaczyć, jak babel-loader został uwzględniony jako reguła:

module: {
  rules: [
    //...
    {
      test: /\.js$/,
      exclude: /node_modules/,
      loader: "babel-loader"
    }
  ]
},
  • @babel/polyfill udostępnia wszystkie wymagane elementy polyfill dla nowszych funkcji ECMAScript, dzięki czemu mogą one działać w środowiskach, które ich nie obsługują. Jest już zaimportowany na samej górze src/index.js.
import "./style.css";
import "@babel/polyfill";
  • @babel/preset-env określa, które przekształcenia i kody polyfill są niezbędne w przypadku przeglądarek i środowisk wybranych jako cele.

Przejrzyj plik konfiguracji Babel (.babelrc), aby zobaczyć, jak jest w nim uwzględniony:

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

To jest konfiguracja Babel i pakietu internetowego. Dowiedz się, jak uwzględnić w aplikacji Babel, jeśli używasz innego pakietu modułów niż Webpack.

Atrybut targets w zasadzie .babelrc określa, na które przeglądarki są kierowane reklamy. @babel/preset-env integruje się z listą przeglądarek, co oznacza, że pełną listę zgodnych zapytań, których można użyć, znajduje się w tym polu w dokumentacji listy przeglądarek.

Wartość "last 2 versions" transpiluje kod w aplikacji dla 2 ostatnich wersji każdej przeglądarki.

Debugowanie

Aby uzyskać pełny wgląd w wszystkie cele Babel w przeglądarce oraz wszystkie uwzględnione przekształcenia i elementy polyfill, dodaj do .babelrc: pole debug

{
  "presets": [
    [
      "@babel/preset-env",
      {
        "targets": "last 2 versions",
        "debug": true
      }
    ]
  ]
}
  • Kliknij Narzędzia.
  • Kliknij Logi.

Załaduj ponownie aplikację i sprawdź logi stanu błędu u dołu edytora.

Docelowe przeglądarki

Babel rejestruje w konsoli szereg informacji o procesie kompilacji, w tym o wszystkich środowiskach docelowych, na które skompilowano kod.

Docelowe przeglądarki

Zwróć uwagę, że na tej liście znajdują się wycofane przeglądarki, takie jak Internet Explorer. Jest to problem, ponieważ w nieobsługiwanych przeglądarkach nie będą dodawane nowsze funkcje, a Babel nadal transpiluje dla nich określoną składnię. Niepotrzebnie zwiększa to rozmiar pakietu, jeśli użytkownicy nie korzystają z witryny za pomocą tej przeglądarki.

Babel rejestruje też listę używanych wtyczek przekształcania:

Lista używanych wtyczek

To długa lista. To wszystkie wtyczki, których Babel potrzebuje do przekształcenia dowolnej składni ES2015+ na starszą w przypadku wszystkich docelowych przeglądarek.

Babel nie pokazuje jednak żadnych konkretnych użytych elementów polyfill:

Nie dodano kodu polyfill

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

Wczytywanie elementów polyfill pojedynczo

Domyślnie Babel uwzględnia wszystkie elementy polyfill niezbędne do działania środowiska ES2015+, gdy @babel/polyfill jest importowany do pliku. Aby zaimportować konkretne elementy polyfill wymagane przez przeglądarki docelowe, 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 elementy polyfill:

Lista zaimportowanych komponentów polyfill

Obecnie uwzględniamy tylko elementy polyfill dla "last 2 versions", ale i tak jest to bardzo długa lista. Dzieje się tak, ponieważ elementy polyfill wymagane w przeglądarkach docelowych w przypadku każdej nowszej funkcji są nadal uwzględniane. Zmień wartość atrybutu na usage, aby uwzględnić tylko te 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 kod polyfill jest dodawany automatycznie tam, gdzie jest to konieczne. Oznacza to, że możesz usunąć importowany plik @babel/polyfill w: src/index.js.

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

Teraz uwzględnione są tylko elementy polyfill wymagane przez aplikację.

Lista automatycznie uwzględnianych zasobów polyfill

Rozmiar pakietu aplikacji jest znacznie mniejszy.

Rozmiar pakietu zmniejszono do 30,1 KB

Zawężanie listy obsługiwanych przeglądarek

Liczba uwzględnionych celów przeglądarek jest nadal bardzo duża i niewielu użytkowników korzysta z wycofanych przeglądarek, takich jak Internet Explorer. Zaktualizuj konfiguracje:

{
  "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

Ponieważ aplikacja jest tak mała, te zmiany nie oznaczają znaczącej różnicy. Zalecamy jednak użycie procentu udziału przeglądarek w rynku (np. ">0.25%") oraz wykluczenie określonych przeglądarek, w przypadku których masz pewność, że użytkownicy nie używają. Aby dowiedzieć się więcej na ten temat, przeczytaj artykuł „Ostatnie 2 wersje” uznany za szkodliwy przez Jamesa Kyle'a.

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

Co jeszcze można poprawić. Usunęliśmy już pewną liczbę nieużywanych elementów polyfill, ale wiele z nich zostało już wysłanych, które nie są potrzebne niektórym przeglądarkom. Dzięki nim można zapisywać nowszą składnię i przesyłać ją bezpośrednio do przeglądarek bez konieczności stosowania zbędnych elementów polyfill.

Moduły JavaScript to stosunkowo nowa funkcja obsługiwana we wszystkich najpopularniejszych przeglądarkach. Moduły można tworzyć za pomocą atrybutu type="module", aby definiować skrypty importu i eksportu 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 obsługujących moduły JavaScript (nie wymaga więc użycia Babel). Oznacza to, że konfigurację Babel można zmodyfikować w taki sposób, aby wysyłać do przeglądarki 2 różne wersje aplikacji:

  • Wersja działająca w nowszych przeglądarkach obsługujących moduły, która zawiera moduł, który w większości nie jest przetłumaczony, ale ma mniejszy rozmiar pliku.
  • wersję zawierającą większy, transpilowany skrypt, który działałby w każdej starszej przeglądarce;

Używanie modułów ES z użyciem Babel

Aby mieć osobne ustawienia @babel/preset-env dla 2 wersji aplikacji, usuń plik .babelrc. Ustawienia Babel można dodać do konfiguracji pakietu internetowego, 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" używana jest wartość esmodules z wartością false. Oznacza to, że Babel uwzględnia wszystkie niezbędne przekształcenia i kody polyfill, które są kierowane na każdą przeglądarkę, która jeszcze nie obsługuje modułów ES.

Dodaj obiekty entry, cssRule i corePlugins na początku pliku webpack.config.js. Są one współdzielone przez moduł i starsze skrypty udostępniane przez przeglądarkę.

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 config dla skryptu modułu poniżej, gdzie zdefiniowany jest atrybut 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 nazwa pliku wyjściowego ma rozszerzenie .mjs. Wartość esmodules w tym module ma wartość Prawda, co oznacza, że kod zawarty w tym module jest mniejszy i mniej skompilowany skrypt w tym przykładzie, który nie przechodzi żadnego przekształcenia, ponieważ wszystkie używane funkcje są już obsługiwane w przeglądarkach, które obsługują moduły.

Na samym końcu pliku wyeksportuj obie konfiguracje w postaci jednej tablicy.

module.exports = [
  legacyConfig, moduleConfig
];

Teraz powstaje zarówno mniejszy moduł dla przeglądarek, które go obsługują, jak i większy transpilowany skrypt dla starszych przeglądarek.

Przeglądarki obsługujące moduły ignorują skrypty z atrybutem nomodule. Natomiast przeglądarki, które nie obsługują modułów, ignorują elementy skryptu z parametrem type="module". Oznacza to, że możesz dołączyć moduł oraz skompilowaną kreację zastępczą. W idealnym przypadku 2 wersje aplikacji powinny znajdować się w pliku index.html w taki sposób:

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

Przeglądarki, które obsługują moduły, pobierają i uruchamiają main.mjs, a potem ignorująmain.bundle.js. Przeglądarki, które nie obsługują modułów, robią odwrotnie.

Należy pamiętać, że w przeciwieństwie do zwykłych skryptów skrypty modułów są zawsze domyślnie odraczane. Jeśli chcesz, by równoważny skrypt nomodule również był odroczony i uruchamiany tylko po przeanalizowaniu, musisz dodać atrybut defer:

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

Musisz jeszcze tylko dodać atrybuty module i nomodule odpowiednio do modułu i starszego skryptu oraz zaimportować ScriptExtHtmlWebpackPlugin na samej górze elementu 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, tak aby zawierała 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 oraz nomodule we wszystkich modułach skryptu .js.

Wyświetlane moduły w dokumencie HTML

Ostatnią rzeczą, jaką musisz zrobić, jest umieszczenie w pliku HTML zarówno starszych, jak i nowoczesnych elementów skryptu. Wtyczka, która tworzy końcowy plik HTML (HTMLWebpackPlugin), obecnie nie obsługuje danych wyjściowych zarówno modułu, jak i skryptów nomodule. Chociaż istnieją obejścia i osobne wtyczki, które pozwalają rozwiązać ten problem, takie jak BabelMultiTargetPlugin i HTMLWebpackMultiBuildPlugin, w tym samouczku zastosowano prostszą metodę ręcznego dodawania elementu skryptu modułu.

Dodaj do src/index.js na końcu pliku ten kod:

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

Teraz otwórz aplikację w przeglądarce, która obsługuje moduły, takie jak najnowsza wersja Chrome.

Moduł o wielkości 5,2 KB został pobrany przez sieć w przypadku nowszych przeglądarek

Pobierany jest tylko moduł. Rozmiar pakietu jest znacznie mniejszy, ponieważ w dużej mierze nie został przetworzony. Drugi element skryptu jest całkowicie ignorowany przez przeglądarkę.

Jeśli wczytujesz aplikację w starszej przeglądarce, zostanie pobrany tylko większy, transpilowany skrypt ze wszystkimi potrzebnymi elementami polyfill i przekształceniami. Oto zrzut ekranu przedstawiający wszystkie żądania przesłane w starszej wersji Chrome (wersja 38).

W przypadku starszych przeglądarek został pobrany skrypt o wielkości 30 KB

Podsumowanie

Wiesz już, jak korzystać z pola @babel/preset-env, aby udostępniać tylko niezbędne elementy polyfill wymagane w przeglądarkach, na które są kierowane reklamy. Wiesz też, jak moduły JavaScript mogą jeszcze bardziej zwiększyć wydajność dzięki dostarczaniu 2 różnych transpilowanych wersji aplikacji. Wiedząc, jak obie te metody mogą znacznie zmniejszyć pakiet, możesz przejść do optymalizacji.