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

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.

Zrzut ekranu aplikacji

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ę:

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

Żądanie oryginalnego rozmiaru pakietu

Ta aplikacja zajmuje ponad 80 KB. Sprawdź, czy niektóre części pakietu nie są używane:

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

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

  3. Na karcie Pokrycie kliknij Załaduj ponownie, aby ponownie załadować aplikację podczas rejestrowania pokrycia.

    Ponowne wczytywanie aplikacji z informacjami o pokryciu kodu

  4. Sprawdź, ile kodu zostało użyte w porównaniu z ilością kodu wczytanego w głównym pakiecie:

    Pokrycie kodu pakietu

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ątku src/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.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.

Przeglądarki, na które można kierować reklamy

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:

Lista używanych wtyczek

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:

Nie dodano żadnych 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:

Lista zaimportowanych polyfilli

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/polyfillsrc/index.js..

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

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

Lista automatycznie dołączanych polyfilli

Rozmiar pakietu aplikacji jest znacznie mniejszy.

Rozmiar pakietu zmniejszony do 30,1 KB

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.

Rozmiar pakietu: 30,0 KB

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, cssRulecorePlugins 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 modulenomodule 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. BabelMultiTargetPluginHTMLWebpackMultiBuildPlugin, 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.

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

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

W przypadku starszych przeglądarek pobierany jest skrypt o rozmiarze 30 KB.

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