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

Zwiększ wydajność, włączając nowoczesne zależności i wyjścia JavaScript.

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 internetowe, takie jak Chrome, Edge, Firefox i Safari, stanowią ponad 90% rynku przeglądarek, a inne przeglądarki korzystające z tych samych silników renderowania stanowią dodatkowe 5%. 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 rozkładu (ES2015)
  • Krótkie nazwy obiektów (ES2015)
  • Async/await (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 starszej składni, aby zwiększyć obsługę przeglądarek. Kompilowanie za pomocą starszej składni zwiększa obsługę w przeglądarkach, ale efekt jest często mniejszy, niż się spodziewamy. 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% kodu JavaScripta używanego w produkcji. 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 Node.js ustandaryzował pole "exports", aby definiować 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. Użytkownicy pakietu muszą zakładać, że moduły z polem "exports" zawierają nowoczesny kod i w razie potrzeby są transpilowane.

Tylko nowoczesne

Jeśli chcesz opublikować pakiet z nowoczesnym kodem i zostawić przetwarzanie na potrzeby konsumenta, gdy używa 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" razem 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 nadal pakiet starszy, który nie zawiera żadnego nowoczesnego kodu poza składnią import/export. Dlatego użyj tego podejścia, aby przesłać nowoczesny kod z użyciem starszego kodu zapasowego, który jest nadal optymalizowany pod kątem pakietów.

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 powodowania błędów w aplikacji w starszych przeglądarkach. Ogólna idea polega na tym, aby system kompilacji przetłumaczył zależność do tej samej składni docelowej co kod źródłowy.

webpack

Od wersji webpack 5 można skonfigurować, jakiej składni webpack ma używać podczas generowania kodu dla pakietów i modułów. Nie powoduje to transpilacji kodu ani zależności, tylko wpływa na kod „klejący” wygenerowany przez webpack. 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 owijające, gdy kierujesz się na nowoczesne środowisko ES Modules. Spowoduje to też skonfigurowanie webpacka do ładowania pakietów z podzielonym kodem 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 webpack, która zamienia końcowy pakiet kodu z nowoczesnego na starszy JavaScript, 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 Plugin działa na pakietach, a nie na poszczególnych modułach, przetwarza kod aplikacji i jej zależności 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ę. Może też być szybszy niż tradycyjne rozwiązania, które wymagają 2 etapów kompilacji, a jednocześnie generuje oddzielne pakiety dla nowoczesnych i starszych przeglądarek. Oba zestawy pakietów zostały zaprojektowane do wczytania za pomocą wzorca module/nomodule.

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

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

Optimize Plugin może być szybszy i bardziej wydajny niż niestandardowe konfiguracje webpacka, które zwykle łączą nowoczesny i stary kod osobno. Narzędzie to obsługuje też uruchamianie Babel i skompresowanie pakietów za pomocą Terser z osobnymi optymalnymi ustawieniami dla nowoczesnych i starszych wyjść. W końcu polyfille potrzebne do wygenerowanych pakietów z wersji starszych są wyodrębniane w specjalnym skrypcie, dzięki czemu nie są dublowane ani nie są 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 webpack, która współpracuje z @babel/preset-env, aby generować nowoczesne wersje dotychczasowych pakietów i przesyłać do nowoczesnych przeglądarek mniej przetłumaczonego kodu. Jest to najpopularniejsze gotowe rozwiązanie dla module/nomodule, używane przez 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 wersje aplikacji. Kompilacja dwukrotna 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.

Konfigurowanie babel-loadera do transpilacji 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 modułami i bez nich, 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 do kompilowania 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". Pomijając buforowanie ze względu na zwiększenie zwiękłości, niestandardowa implementacja 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 takim przypadku musisz się upewnić, że Twój minifikator obsługuje współczesną składnię. Zarówno Terser, jak i uglify-es mają opcję określania {ecma: 2017}, aby 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. Możesz użyć tej funkcji, aby wygenerować różne pakiety dla wersji nowoczesnej i starszej, przekazując je przez różne konfiguracje wtyczki 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 do kompilacji na wyższym poziomie, 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 webpacka i Rollupa do każdego projektu można dodać nowoczesne pakiety JavaScriptu z przestarzałymi wersjami zapasowymi, korzystając z ewolucji. Devolution to samodzielne narzędzie, które przekształca dane wyjściowe z systemu kompilacji w wersje JavaScriptu w starszej wersji, co umożliwia łączenie i przekształcanie w celu uzyskania nowoczesnego wyjścia.