Publikuj, wysyłaj i instaluj nowoczesny kod JavaScript, aby przyspieszyć działanie aplikacji

Aby poprawić wydajność, włącz nowoczesne zależności i dane wyjściowe JavaScriptu.

Ponad 90% przeglądarek obsługuje nowoczesny JavaScript, ale obecnie w internecie nadal dominują starsze skrypty JavaScript, które są głównym źródłem problemów z wydajnością.

Nowoczesny kod JavaScript

Nowoczesny JavaScript nie jest opisywany jako kod napisany w określonej wersji specyfikacji ECMAScript, ale raczej w składni obsługiwanej przez wszystkie nowoczesne przeglądarki. Nowoczesne przeglądarki, takie jak Chrome, Edge, Firefox i Safari, stanowią ponad 90% rynku przeglądarek, a kolejne 5% stanowią przeglądarki korzystające z tych samych bazowych mechanizmów renderowania. Oznacza to, że 95% globalnego ruchu internetowego pochodzi z przeglądarek, które obsługują najczęściej używane funkcje języka JavaScript z ostatnich 10 lat, w tym:

  • Klasy (ES2015)
  • Funkcje strzałek (ES2015)
  • Generatory (ES2015)
  • Określanie zakresu bloku (ES2015)
  • Restrukturyzacja (ES2015)
  • Parametry odpoczynku i rozchodzenia się (ES2015)
  • Krótkie nazwy obiektów (ES2015)
  • Asynchroniczne/oczekujące (ES2017)

Funkcje w nowszych wersjach specyfikacji języka są zazwyczaj mniej spójnie obsługiwane w nowoczesnych przeglądarkach. Na przykład wiele funkcji ES2020 i ES2021 jest obsługiwanych tylko na 70% rynku przeglądarek, czyli w większości przeglądarek, ale nie na tyle, aby można było na nich bezpośrednio polegać. Oznacza to, że chociaż „nowoczesny” JavaScript jest ruchomym celem, specyfikacja ES2017 zapewnia najszerszą zgodność z przeglądarkami oraz obejmuje większość powszechnie używanych funkcji nowoczesnej składni. Innymi słowy, ES2017 jest obecnie najbliższy nowoczesnej składni.

Starsza wersja kodu JavaScript

Starsza wersja kodu JavaScript to kod, który nie używa wszystkich wymienionych powyżej funkcji języka. Większość programistów pisze kod źródłowy, używając nowoczesnej składni, ale kompiluje wszystko do składni starszej, aby zwiększyć obsługę przeglądarek. Kompilowanie do starszej składni zwiększa obsługę przeglądarek, ale efekty często są mniejsze, niż zdajemy sobie sprawę. W wielu przypadkach poziom wsparcia wzrasta z około 95% do 98%, a koszt jest znaczny:

  • Starszy kod JavaScript jest zwykle o około 20% większy i wolniejszy niż odpowiedni nowoczesny kod. Niedostatki narzędzi i nieprawidłowa konfiguracja często jeszcze pogłębiają tę różnicę.

  • Zainstalowane biblioteki stanowią nawet 90% typowego kodu JavaScripta w wersji produkcyjnej. Kod biblioteki generuje jeszcze większy narzut związany ze starszymi skryptami JavaScript ze względu na duplikowanie funkcji pomocniczych i elementów polyfill, których można uniknąć, publikując nowoczesny kod.

Nowoczesny JavaScript w npm

Niedawno w Node.js ujednolicono pole "exports", które określa punkty wejścia pakietu:

{
  "exports": "./index.js"
}

Moduł, do którego odwołuje się pole "exports", wymaga wersji Node co najmniej 12.8, która obsługuje ES2019. Oznacza to, że każdy moduł, do którego odwołuje się pole "exports", może być nadpisany w nowoczesnym języku JavaScript. Konsumenci pakietów muszą przyjąć, że moduły z polem "exports" zawierają nowoczesny kod i w razie potrzeby przeprowadzić transpilację.

Tylko nowoczesne

Jeśli chcesz opublikować pakiet z nowoczesnym kodem i pozostawić konsumentowi jego transpilację, gdy używają go jako zależności, użyj tylko pola "exports".

{
  "name": "foo",
  "exports": "./modern.js"
}

Nowoczesny z użyciem starszego kodu

Użyj pola "exports" wraz z polem "main", aby opublikować pakiet za pomocą nowoczesnego kodu, ale uwzględnij też wersję zapasową ES5 + CommonJS dla starszych przeglądarek.

{
  "name": "foo",
  "exports": "./modern.js",
  "main": "./legacy.cjs"
}

Nowoczesny z optymalizacją dotyczącą starszych wersji i pakietu ESM

Oprócz zdefiniowania punktu wejścia zastępczego CommonJS pole "module" może służyć do wskazywania podobnego starszego pakietu zastępczego, ale takiego, który używa składni modułu JavaScript (importexport).

{
  "name": "foo",
  "exports": "./modern.js",
  "main": "./legacy.cjs",
  "module": "./module.js"
}

Wiele narzędzi do tworzenia pakietów, takich jak webpack i Rollup, korzysta z tego pola, aby korzystać z funkcji modułów i umożliwiać usuwanie elementów drzewa. Jest to starszy pakiet, który nie zawiera żadnego nowoczesnego kodu poza składnią import/export, więc użyj tej metody, aby wysyłać nowoczesny kod za pomocą starszej kreacji zastępczej, która jest nadal zoptymalizowana pod kątem łączenia w pakiety.

Nowoczesny JavaScript w aplikacjach

Zależności zewnętrzne stanowią zdecydowaną większość typowego kodu JavaScript w wersji produkcyjnej w aplikacjach internetowych. Chociaż zależności npm były do tej pory publikowane w starszej składni ES5, nie jest to już bezpieczne założenie, ponieważ aktualizacje zależności mogą spowodować utratę obsługi przeglądarki w aplikacji.

Wraz z rosnącą liczbą pakietów npm przenoszonych na nowoczesny JavaScript ważne jest, aby narzędzia do kompilacji były skonfigurowane tak, aby je obsługiwać. Jest duża szansa, że niektóre z używanych przez Ciebie pakietów npm korzystają już z funkcji nowoczesnego języka. Istnieje kilka opcji umożliwiających korzystanie z nowoczesnego kodu z npm bez zakłócania działania aplikacji w starszych przeglądarkach. Ogólna idea polega na tym, aby system kompilacji przetłumaczył zależności do tego samego celu składniowego co kod źródłowy.

webpack

Od wersji webpacka 5 można określić, która składnia pakietu internetowego będzie używana podczas generowania kodu pakietów i modułów. Nie transpiluje to Twojego kodu ani zależności, a jedynie wpływa na kod „glue” wygenerowany przez pakiet internetowy. Aby określić obsługiwane przeglądarki, dodaj do projektu listę przeglądarek lub zrób to bezpośrednio w konfiguracji webpack:

module.exports = {
  target: ['web', 'es2017'],
};

Można też skonfigurować webpack, aby generował zoptymalizowane pakiety, które pomijają niepotrzebne funkcje opakowania, gdy kierujesz się na nowoczesne środowisko ES Modules. Spowoduje to też skonfigurowanie webpacka do ładowania pakietów podzielonych kodu za pomocą <script type="module">.

module.exports = {
  target: ['web', 'es2017'],
  output: {
    module: true,
  },
  experiments: {
    outputModule: true,
  },
};

Dostępnych jest wiele wtyczek webpack, które umożliwiają kompilowanie i przesyłanie nowoczesnego kodu JavaScript przy jednoczesnym zachowaniu obsługi starszych przeglądarek, takich jak Optimize Plugin i BabelEsmPlugin.

Wtyczka Optimize

Optimize Plugin to wtyczka pakietu internetowego, która przekształca ostateczny kod w pakiecie JavaScript z nowoczesnego na starszy, zamiast poszczególnych plików źródłowych. Jest to samodzielna konfiguracja, która pozwala konfiguracji webpacka założyć, że wszystko jest nowoczesnym kodem JavaScript bez specjalnego rozgałęzienia na wiele wyjść lub składni.

Ponieważ wtyczka Optimize działa na pakietach, a nie na poszczególnych modułach, kod aplikacji i zależność są przetwarzane w taki sam sposób. Dzięki temu można bezpiecznie używać nowoczesnych zależności JavaScript z npm, ponieważ ich kod zostanie złączony i przetłumaczony na poprawną składnię. Mogą być też szybsze niż tradycyjne rozwiązania, które składają się z 2 etapów kompilacji, a jednocześnie generować osobne pakiety dla nowoczesnych i starszych przeglądarek. Oba zestawy pakietów zostały zaprojektowane do wczytania za pomocą wzorca moduł/brak modułu.

// webpack.config.js
const OptimizePlugin = require('optimize-plugin');

module.exports = {
  // ...
  plugins: [new OptimizePlugin()],
};

Optimize Plugin może być szybszy i wydajniejszy niż niestandardowe konfiguracje pakietów internetowych, które zwykle łączą w sobie oddzielny kod od starszego. Obsługuje też uruchamianie Babel i minimalizuje pakiety za pomocą usługi Terser z osobnymi optymalnymi ustawieniami dla nowoczesnych i starszych danych wyjściowych. Elementy polyfill potrzebne przez wygenerowane pakiety starszego typu są wyodrębniane do specjalnego skryptu, dzięki czemu nigdy nie są duplikowane ani niepotrzebnie ładowane w nowszych przeglądarkach.

Porównanie: kompilacja modułów źródłowych 2 razy w porównaniu z kompilacją wygenerowanych pakietów.

BabelEsmPlugin

BabelEsmPlugin to wtyczka pakietu internetowego, która współpracuje z pakietem @babel/preset-env i generuje nowoczesne wersje istniejących pakietów, umożliwiając wysyłanie mniej przetranspilowanego kodu do nowoczesnych przeglądarek. To najpopularniejsze, gotowe do użytku rozwiązanie do obsługi modułów i modułów bez modułu, używane w interfejsach Next.js i Preact CLI.

// webpack.config.js
const BabelEsmPlugin = require('babel-esm-plugin');

module.exports = {
  //...
  module: {
    rules: [
      // your existing babel-loader configuration:
      {
        test: /\.js$/,
        exclude: /node_modules/,
        use: {
          loader: 'babel-loader',
          options: {
            presets: ['@babel/preset-env'],
          },
        },
      },
    ],
  },
  plugins: [new BabelEsmPlugin()],
};

BabelEsmPlugin obsługuje wiele konfiguracji webpacka, ponieważ uruchamia 2 oddzielne kompilacje aplikacji. Kompilacja dwukrotnie może zająć trochę więcej czasu w przypadku dużych aplikacji, ale ta technika umożliwia BabelEsmPlugin bezproblemową integrację z dotychczasowymi konfiguracjami webpack i czyni ją jedną z najwygodniejszych dostępnych opcji.

Skonfiguruj moduł babel-loader, aby transpilować moduły node_modules

Jeśli używasz babel-loader bez jednego z tych dwóch wtyczek, musisz wykonać ważny krok, aby korzystać z nowych modułów npm JavaScript. Definiowanie 2 oddzielnych konfiguracji babel-loader umożliwia automatyczne kompilowanie nowoczesnych funkcji językowych z node_modules do ES2017, przy jednoczesnym transpilowaniu własnego kodu własnego w ramach wtyczek i predefiniowanych ustawień Babel zdefiniowanych w konfiguracji projektu. Nie generuje to nowoczesnych i starszych pakietów w przypadku konfiguracji z atrybutami „module” i „nomodule”, ale umożliwia instalowanie i używanie pakietów npm zawierających nowoczesny kod JavaScript bez zakłócania pracy starszych przeglądarek.

webpack-plugin-modern-npm korzysta z tej techniki, aby skompilować zależności npm, które mają pole "exports" w package.json, ponieważ mogą one zawierać nowoczesną składnię:

// webpack.config.js
const ModernNpmPlugin = require('webpack-plugin-modern-npm');

module.exports = {
  plugins: [
    // auto-transpile modern stuff found in node_modules
    new ModernNpmPlugin(),
  ],
};

Możesz też ręcznie zastosować tę metodę w konfiguracji webpack, sprawdzając, czy w polu package.json w module package.json występuje pole "exports". Pominięcie buforowania w celu zwięzłości danych może wyglądać tak:

// webpack.config.js
module.exports = {
  module: {
    rules: [
      // Transpile for your own first-party code:
      {
        test: /\.js$/i,
        loader: 'babel-loader',
        exclude: /node_modules/,
      },
      // Transpile modern dependencies:
      {
        test: /\.js$/i,
        include(file) {
          let dir = file.match(/^.*[/\\]node_modules[/\\](@.*?[/\\])?.*?[/\\]/);
          try {
            return dir && !!require(dir[0] + 'package.json').exports;
          } catch (e) {}
        },
        use: {
          loader: 'babel-loader',
          options: {
            babelrc: false,
            configFile: false,
            presets: ['@babel/preset-env'],
          },
        },
      },
    ],
  },
};

W tym przypadku musisz się upewnić, że Twój minifikator obsługuje współczesną składnię. Zarówno Terser, jak i uglify-es mają opcję określenia {ecma: 2017}, która pozwala zachować, a w niektórych przypadkach wygenerować składnię ES2017 podczas kompresji i formatowania.

Podsumowanie

Funkcja łączenia ma wbudowaną obsługę generowania wielu zestawów pakietów w ramach jednej kompilacji i domyślnie generuje nowoczesny kod. Dzięki temu możesz skonfigurować funkcję Rollup tak, aby generowała nowoczesne i starsze pakiety z oficjalnymi wtyczkami, z których prawdopodobnie już korzystasz.

@rollup/plugin-babel

Jeśli używasz Rollup, metoda getBabelOutputPlugin() (dostępna w oficjalnym pluginie Babel Rollup) przekształca kod w wygenerowanych pakietach, a nie w poszczególnych modułach źródłowych. Funkcja łączenia ma wbudowane wsparcie dla generowania wielu zestawów pakietów w ramach jednej kompilacji, z własnymi wtyczkami dla każdego z nich. W ten sposób możesz utworzyć różne pakiety dla nowoczesnych i starszych wersji, przekazując każdy z nich za pomocą innej konfiguracji wtyczki wyjściowej Babel:

// rollup.config.js
import {getBabelOutputPlugin} from '@rollup/plugin-babel';

export default {
  input: 'src/index.js',
  output: [
    // modern bundles:
    {
      format: 'es',
      plugins: [
        getBabelOutputPlugin({
          presets: [
            [
              '@babel/preset-env',
              {
                targets: {esmodules: true},
                bugfixes: true,
                loose: true,
              },
            ],
          ],
        }),
      ],
    },
    // legacy (ES5) bundles:
    {
      format: 'amd',
      entryFileNames: '[name].legacy.js',
      chunkFileNames: '[name]-[hash].legacy.js',
      plugins: [
        getBabelOutputPlugin({
          presets: ['@babel/preset-env'],
        }),
      ],
    },
  ],
};

Dodatkowe narzędzia do kompilacji

Rollup i Webpack mają wiele opcji konfiguracji, co oznacza, że każdy projekt musi zaktualizować swoją konfigurację, aby umożliwić korzystanie z nowoczesnej składni JavaScriptu w zależnych bibliotekach. Istnieją też narzędzia kompilacji wyższego poziomu, które preferują konwencje i wartości domyślne zamiast konfiguracji, takie jak Parcel, Snowpack, ViteWMR. Większość z nich zakłada, że zależności npm mogą zawierać nowoczesną składnię, i przekształca je na odpowiedni poziom składni podczas kompilacji w celu wdrożenia.

Oprócz dedykowanych wtyczek do Webpack i usługi Rollup do każdego projektu można dodać nowoczesne pakiety JavaScript ze starszymi kreacjami zastępczymi. Devolution to samodzielne narzędzie, które przekształca dane wyjściowe z systemu kompilacji, aby tworzyć starsze wersje JavaScriptu, dzięki czemu łączenie ich w pakiety i przekształcenia pozwala przyjąć nowoczesne miejsce docelowe danych wyjściowych.