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

Dzięki tym ćwiczeniom z programowania możesz poprawić wydajność tej prostej aplikacji, która umożliwia użytkownikom ocenianie losowych kotów. Dowiedz się, jak zoptymalizować pakiet JavaScriptu, ograniczając do minimum ilość transpilowanego kodu.

Zrzut ekranu aplikacji

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

Zmierz odległość

Zawsze warto zacząć od sprawdzenia witryny przed dodaniem jakichkolwiek optymalizacji:

  1. Aby wyświetlić podgląd strony, kliknij Wyświetl aplikację, a potem 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ę.

Pierwotne żądanie dotyczące rozmiaru pakietu

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

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

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

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

    Załaduj ponownie aplikację z pokryciem kodu

  4. Sprawdź, ile kodu został użyty, a ile został wczytany w przypadku pakietu głównego:

    Zasięg kodu pakietu

Ponad połowa pakietu (44 KB) nawet nie jest wykorzystywana. Dzieje się tak, ponieważ znaczna część kodu zawiera elementy polyfill, które zapewniają działanie aplikacji w starszych przeglądarkach.

Użyj @babel/gotowe-env

Składnia języka JavaScript jest zgodna ze standardem ECMAScript lub ECMA-262. Co roku publikowane są nowsze wersje specyfikacji. Zawierają one nowe funkcje, które przeszły proces tworzenia oferty pakietowej. Każda z głównych przeglądarek jest zawsze na innym etapie obsługi tych funkcji.

Aplikacja używa tych funkcji ES2015:

Używana jest też następująca funkcja ES2017:

Możesz zagłębić się w kod źródłowy w src/index.js, aby zobaczyć, jak wszystkie te funkcje są wykorzystywane.

Wszystkie te funkcje działają w najnowszej wersji Chrome. Co jednak z innymi przeglądarkami, które ich nie obsługują? Biblioteka Babel, która jest częścią aplikacji, jest najpopularniejszą biblioteką używaną do kompilowania kodu o nowszej składni w postaci kodu zrozumiałego dla starszych przeglądarek i środowisk. Odbywa się to na 2 sposoby:

  • Obiekty Polyfill są dołączone do emulacji nowszych funkcji ES2015+, dzięki czemu ich interfejsy API mogą być używane, nawet jeśli nie są obsługiwane przez przeglądarkę. Oto przykład użycia polyfill 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ą (takie jak funkcje strzałek), dlatego nie można ich emulować za pomocą elementów polyfill.

Otwórz package.json, aby sprawdzić, które biblioteki Babel zostały 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 jest głównym kompilatorem Babel. Dzięki temu wszystkie konfiguracje Babel są zdefiniowane w pliku .babelrc na poziomie głównym projektu.
  • babel-loader włącza Babel w proces kompilacji pakietu webpack.

Przyjrzyj się teraz webpack.config.js, aby zobaczyć, w jaki sposób reguła babel-loader jest uwzględniona w zasadzie:

module: {
  rules: [
    //...
    {
      test: /\.js$/,
      exclude: /node_modules/,
      loader: "babel-loader"
    }
  ]
},
  • @babel/polyfill zapewnia wszystkie wymagane elementy polyfill na potrzeby wszystkich nowszych funkcji ECMAScript, aby umożliwić działanie w środowiskach, które ich nie obsługują. Jest już zaimportowany na samej górze strony src/index.js.
import "./style.css";
import "@babel/polyfill";
  • @babel/preset-env wskazuje, które przekształcenia i kody polyfill są konieczne we wszystkich przeglądarkach lub środowiskach wybranych jako cele.

Zobacz plik konfiguracji Babel (.babelrc), aby zobaczyć, jak znajduje się w niej:

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

To konfiguracja Babel i Webpack. Dowiedz się, jak dodać Babel do aplikacji, jeśli używasz innego pakietu modułów niż webpack.

Atrybut targets w .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ć, znajdziesz 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 zobaczyć wszystkie cele Babel w przeglądarce oraz wszystkie przekształcenia i kody polyfill w przeglądarce, dodaj pole debug do parametru .babelrc:

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

Załaduj ponownie aplikację i sprawdź dzienniki stanu Usterki u dołu edytora.

Docelowe przeglądarki

Babel zapisuje w konsoli szereg szczegółów dotyczących procesu kompilacji, w tym wszystkich środowisk docelowych, dla których 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 są dodawane nowsze funkcje, a Babel cały czas tworzy dla nich transpilację określonej składni. To niepotrzebnie zwiększa rozmiar pakietu, jeśli użytkownicy nie używają tej przeglądarki do uzyskiwania dostępu do Twojej witryny.

Babel rejestruje również listę używanych wtyczek przekształcania:

Lista używanych wtyczek

To dość długa lista. To wszystkie wtyczki, których Babel potrzebuje do przekształcenia składni ES2015 i starszych w starszą składnię dla 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.

Ładuj elementy polyfill pojedynczo

Podczas importowania do pliku Babel domyślnie uwzględnia wszystkie elementy polyfill potrzebne do działania pełnego środowiska ES2015+.@babel/polyfill Aby zaimportować określone kody polyfill potrzebne w przeglądarkach docelowych, dodaj do konfiguracji 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 elementy polyfill:

Lista zaimportowanych kodu polyfill

Chociaż w polu "last 2 versions" znajdują się teraz tylko elementy polyfill potrzebne, lista nadal jest bardzo długa. Dzieje się tak, ponieważ kody polyfill wymagane w przeglądarkach docelowych są nadal obecne w każdej nowszej funkcji. Zmień wartość atrybutu na usage, aby uwzględnić tylko te informacje, które są niezbędne w przypadku cech używanych w kodzie.

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

Dzięki temu adresy polyfill są dodawane automatycznie, gdy jest to wymagane. Oznacza to, że możesz usunąć import @babel/polyfill w src/index.js.

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

Teraz aplikacja zawiera tylko wymagane kody polyfill.

Lista elementów polyfill automatycznie uwzględnianych

Rozmiar pakietu aplikacji jest znacznie zmniejszony.

Zmniejszono rozmiar pakietu do 30,1 KB

Zawężanie listy obsługiwanych przeglądarek

Liczba uwzględnionych w przeglądarkach jest wciąż bardzo duża i niewielu użytkowników korzysta z wycofywanych przeglądarek, takich jak Internet Explorer. Zmień konfigurację na taką:

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

Spójrz na szczegóły pobranego pakietu.

Rozmiar pakietu 30.0 KB

Ponieważ aplikacja jest bardzo mała, wprowadzone zmiany nie mają większego znaczenia. Zalecamy jednak użycie procentowego udziału przeglądarek w rynku (np. ">0.25%") i wykluczenie określonych przeglądarek, których użytkownicy prawdopodobnie nie używają. Aby dowiedzieć się więcej, przeczytaj artykuł Jamesa Kyle'a o „2 ostatnich wersjach” uznanych za szkodliwe.

Użyj modułu <script type="module">

Jest wiele rzeczy, które można poprawić. Usunęliśmy wiele nieużywanych elementów polyfill, jednak niektóre z nich nie są niezbędne w niektórych przeglądarkach. Dzięki nim można zapisywać nowszą składnię i przesyłać ją bezpośrednio do przeglądarek bez konieczności używania zbędnych elementów polyfill.

Moduły JavaScript to stosunkowo nowa funkcja obsługiwana we wszystkich popularnych przeglądarkach. Moduły można tworzyć za pomocą atrybutu type="module", aby definiować skrypty, które są importowane i eksportowane 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 (zamiast stosowania Babel). Oznacza to, że konfigurację Babel można zmodyfikować tak, aby wysyłała do przeglądarki 2 różne wersje aplikacji:

  • Wersja, która będzie działać w nowszych przeglądarkach obsługujących moduły i zawierająca moduł, który w dużej mierze nie jest transpilowany, ale ma mniejszy rozmiar
  • Wersja zawierająca większy, transpilowany skrypt, który działa w dowolnej starszej przeglądarce

Korzystanie z 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 pakietu webpack, 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 parametru "@babel/preset-env" używana jest wartość esmodules z wartością false. Oznacza to, że zawiera wszystkie niezbędne przekształcenia i kody polyfill pozwalające kierować reklamy 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ółużytkowane przez moduł i starsze skrypty wyświetlane 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"})
];

W podobny sposób utwórz obiekt konfiguracji dla skryptu modułu poniżej, gdzie 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 jako nazwę pliku wyjściowego używane jest rozszerzenie pliku .mjs. Wartość esmodules ma tutaj wartość „true” (prawda), co oznacza, że kod umieszczane w tym module jest mniejszy i mniej skompilowany skrypt, który nie przechodzi w tym przykładzie żadnego przekształcenia, ponieważ wszystkie używane funkcje są już obsługiwane w przeglądarkach obsługujących moduły.

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

module.exports = [
  legacyConfig, moduleConfig
];

Teraz powstaje zarówno mniejszy moduł dla przeglądarek, które go obsługują, jak i 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 parametrem type="module". Oznacza to, że możesz uwzględnić moduł, a także skompilowaną kreację zastępczą. Najlepiej byłoby, gdyby 2 wersje aplikacji znalazły się w tym języku: index.html:

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

Przeglądarki obsługujące moduły pobierają i wykonują funkcje main.mjs oraz ignorują je. main.bundle.js. Przeglądarki, które nie obsługują modułów, działają odwrotnie.

Warto zauważyć, że w przeciwieństwie do zwykłych skryptów skrypty modułu są zawsze domyślnie odroczone. Jeśli chcesz, aby odpowiednik skryptu nomodule również był odroczony i wykonywany tylko po przeanalizowaniu, musisz dodać atrybut defer:

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

Ostatnią czynnością, jaką należy zrobić, jest dodanie atrybutów module i nomodule do modułu i starszego skryptu oraz zaimportowanie ScriptExtHtmlWebpackPlugin na samej górze witryny 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" dla wszystkich elementów skryptu .mjs oraz atrybut nomodule dla wszystkich modułów skryptu .js.

Moduły udostępniania w dokumencie HTML

Ostatnią czynnością, jaką należy 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 modułu ani skryptów nomodule. Istnieją obejścia tego problemu i oddzielne wtyczki (np. BabelMultiTargetPlugin i HTMLWebpackMultiBuildPlugin), jednak w tym samouczku używamy prostszego sposobu ręcznego dodawania elementu skryptu modułu.

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

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

Teraz załaduj aplikację w przeglądarce, która obsługuje moduły, na przykład w najnowszej wersji Chrome.

Moduł 5,2 KB został pobrany przez sieć dla nowszych przeglądarek

Pobrany został tylko moduł, który zawiera znacznie mniejszy pakiet, ponieważ w większości nie jest przetranspilowany. Drugi element skryptu jest całkowicie ignorowany przez przeglądarkę.

Jeśli załadujesz aplikację w starszej przeglądarce, zostanie pobrany tylko większy, transpilowany skrypt ze wszystkimi potrzebnymi kodami polyfill i przekształcenia. Oto zrzut ekranu wszystkich żądań wysłanych w starszej wersji Chrome (wersja 38).

Pobrano skrypt 30 KB w przypadku starszych przeglądarek

Podsumowanie

Wiesz już, jak używać @babel/preset-env, by dostarczać tylko niezbędne kody polyfill potrzebne w przeglądarkach docelowych. Wiesz też, jak moduły JavaScript mogą zwiększyć wydajność, wysyłając 2 różne transpilowane wersje aplikacji. Jeśli wiesz, w jaki sposób obie te metody mogą znacząco zmniejszyć rozmiar pakietu, zacznij optymalizować pakiet.