Publikasikan, kirimkan, dan instal JavaScript modern untuk aplikasi yang lebih cepat

Tingkatkan performa dengan mengaktifkan dependensi dan output JavaScript modern.

Lebih dari 90% browser mampu menjalankan JavaScript modern, tetapi prevalensi JavaScript lama tetap menjadi sumber besar masalah performa di web saat ini.

JavaScript Modern

JavaScript modern tidak dicirikan sebagai kode yang ditulis dalam versi spesifikasi ECMAScript tertentu, melainkan dalam sintaksis yang didukung oleh semua browser modern. Browser web modern seperti Chrome, Edge, Firefox, dan Safari menempati lebih dari 90% pasar browser, dan beberapa browser yang mengandalkan mesin rendering dasar yang sama menghasilkan 5% tambahan. Artinya, 95% traffic web global berasal dari browser yang mendukung fitur bahasa JavaScript yang paling banyak digunakan dalam 10 tahun terakhir, termasuk:

  • Kelas (ES2015)
  • Fungsi panah (ES2015)
  • Generator (ES2015)
  • Cakupan blok (ES2015)
  • Destrukturisasi (ES2015)
  • Parameter istirahat dan penyebaran (ES2015)
  • Singkatan objek (ES2015)
  • Async/await (ES2017)

Fitur dalam versi spesifikasi bahasa yang lebih baru umumnya memiliki dukungan yang kurang konsisten di seluruh browser modern. Misalnya, banyak fitur ES2020 dan ES2021 hanya didukung di 70% pasar browser—masih sebagian besar browser, tetapi tidak cukup aman untuk mengandalkan fitur tersebut secara langsung. Artinya, meskipun JavaScript "modern" menjadi target bergerak, ES2017 memiliki rentang kompatibilitas browser terluas sekaligus menyertakan sebagian besar fitur sintaksis modern yang umum digunakan. Dengan kata lain, ES2017 adalah sintaksis yang paling mendekati sintaksis modern saat ini.

JavaScript Lama

JavaScript lama adalah kode yang secara khusus menghindari penggunaan semua fitur bahasa di atas. Sebagian besar developer menulis kode sumbernya menggunakan sintaksis modern, tetapi mengompilasi semuanya ke sintaksis lama untuk meningkatkan dukungan browser. Kompilasi ke sintaksis lama akan meningkatkan dukungan browser, tetapi dampaknya sering kali lebih kecil dari yang kita sadari. Dalam banyak kasus, dukungan meningkat dari sekitar 95% menjadi 98% sekaligus menimbulkan biaya yang signifikan:

  • JavaScript lama biasanya sekitar 20% lebih besar dan lebih lambat daripada kode modern yang setara. Kekurangan dan kesalahan konfigurasi alat sering kali semakin memperlebar jarak ini.

  • Library yang diinstal menyumbang hingga 90% kode JavaScript produksi standar. Kode library menimbulkan overhead JavaScript lama yang lebih tinggi lagi karena duplikasi polyfill dan helper yang dapat dihindari dengan memublikasikan kode modern.

JavaScript modern di npm

Baru-baru ini, Node.js telah menstandarkan kolom "exports" untuk menentukan titik entri untuk paket:

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

Modul yang dirujuk oleh kolom "exports" menunjukkan versi Node setidaknya 12.8, yang mendukung ES2019. Ini berarti setiap modul yang direferensikan menggunakan kolom "exports" dapat ditulis dalam JavaScript modern. Konsumen paket harus mengasumsikan modul dengan kolom "exports" berisi kode modern dan transpilasi jika diperlukan.

Khusus modern

Jika Anda ingin memublikasikan paket dengan kode modern dan menyerahkannya kepada konsumen untuk menangani transpilasi paket saat mereka menggunakannya sebagai dependensi—hanya gunakan kolom "exports".

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

Modern dengan penggantian lama

Gunakan kolom "exports" bersama dengan "main" untuk memublikasikan paket Anda menggunakan kode modern, tetapi juga menyertakan penggantian ES5 + CommonJS untuk browser lama.

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

Modern dengan penggantian lama dan pengoptimalan pemaket ESM

Selain menentukan entri CommonJS penggantian, kolom "module" dapat digunakan untuk mengarah ke paket penggantian lama yang serupa, tetapi menggunakan sintaksis modul JavaScript (import dan export).

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

Banyak pemaket, seperti webpack dan Rollup, mengandalkan kolom ini untuk memanfaatkan fitur modul dan mengaktifkan tree shaking. Paket ini masih berupa paket lama yang tidak berisi kode modern selain sintaksis import/export. Jadi, gunakan pendekatan ini untuk mengirimkan kode modern dengan penggantian lama yang masih dioptimalkan untuk pemaketan.

JavaScript modern dalam aplikasi

Dependensi pihak ketiga merupakan sebagian besar kode JavaScript produksi umum di aplikasi web. Meskipun dependensi npm secara historis telah dipublikasikan sebagai sintaksis ES5 lama, hal ini bukan lagi asumsi yang aman dan update dependensi berisiko merusak dukungan browser dalam aplikasi Anda.

Dengan semakin banyaknya paket npm yang berpindah ke JavaScript modern, penting untuk memastikan bahwa alat build sudah disiapkan untuk menanganinya. Kemungkinan besar beberapa paket npm yang Anda andalkan sudah menggunakan fitur bahasa modern. Ada sejumlah opsi yang tersedia untuk menggunakan kode modern dari npm tanpa merusak aplikasi Anda di browser lama, tetapi ide umumnya adalah membuat sistem build melakukan transpilasi dependensi ke target sintaksis yang sama dengan kode sumber Anda.

webpack

Mulai dari webpack 5, kini Anda dapat mengonfigurasi sintaksis yang akan digunakan webpack saat membuat kode untuk paket dan modul. Tindakan ini tidak mentranspilasi kode atau dependensi Anda, tetapi hanya memengaruhi kode "glue" yang dihasilkan oleh webpack. Untuk menentukan target dukungan browser, tambahkan konfigurasi browserslist ke project Anda, atau lakukan langsung di konfigurasi webpack Anda:

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

Anda juga dapat mengonfigurasi webpack untuk menghasilkan paket yang dioptimalkan dan menghilangkan fungsi wrapper yang tidak perlu saat menargetkan lingkungan Modul ES modern. Tindakan ini juga akan mengonfigurasi webpack untuk memuat paket pemisahan kode menggunakan <script type="module">.

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

Ada sejumlah plugin webpack yang tersedia yang memungkinkan untuk mengompilasi dan mengirim JavaScript modern sambil tetap mendukung browser lama, seperti Plugin Optimize dan BabelEsmPlugin.

Plugin Optimize

Plugin Optimize adalah plugin webpack yang mengubah kode paket akhir dari JavaScript modern ke versi lama, bukan setiap file sumber individual. Ini adalah penyiapan mandiri yang memungkinkan konfigurasi webpack Anda mengasumsikan bahwa semuanya adalah JavaScript modern tanpa percabangan khusus untuk beberapa output atau sintaksis.

Karena Plugin Optimize beroperasi dalam paket, bukan modul individual, Plugin Optimize memproses kode aplikasi dan dependensi Anda secara merata. Dengan begitu, penggunaan dependensi JavaScript modern dari npm menjadi aman karena kodenya akan dipaketkan dan ditranspilasi ke sintaksis yang benar. Cara ini juga dapat lebih cepat daripada solusi tradisional yang melibatkan dua langkah kompilasi, sambil tetap membuat paket terpisah untuk browser modern dan lama. Kedua kumpulan paket ini dirancang untuk dimuat menggunakan pola modul/nomodule.

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

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

Optimize Plugin dapat lebih cepat dan lebih efisien daripada konfigurasi webpack kustom, yang biasanya menggabungkan kode modern dan kode lama secara terpisah. API ini juga menangani Babel yang berjalan untuk Anda, dan meminifikasi paket menggunakan Terser dengan setelan optimal yang terpisah untuk output modern dan lama. Terakhir, polyfill yang diperlukan oleh paket lama yang dihasilkan diekstrak ke dalam skrip khusus sehingga tidak pernah diduplikasi atau tidak perlu dimuat di browser yang lebih baru.

Perbandingan: melakukan transpilasi modul sumber dua kali dibandingkan mentranspilasi paket yang dihasilkan.

BabelEsmPlugin

BabelEsmPlugin adalah plugin webpack yang berfungsi bersama dengan @babel/preset-env untuk menghasilkan versi modern paket yang ada guna mengirimkan lebih sedikit kode yang ditranspilasi ke browser modern. Ini adalah solusi siap pakai yang paling populer untuk modul/nomodule, yang digunakan oleh Next.js dan 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 mendukung beragam konfigurasi webpack karena menjalankan dua build aplikasi yang sebagian besar terpisah. Mengompilasi dua kali dapat memerlukan sedikit waktu tambahan untuk aplikasi besar, tetapi teknik ini memungkinkan BabelEsmPlugin terintegrasi dengan lancar ke dalam konfigurasi webpack yang ada dan menjadikannya salah satu opsi paling mudah yang tersedia.

Mengonfigurasi babel-loader untuk mentranspilasi node_modules

Jika Anda menggunakan babel-loader tanpa salah satu dari dua plugin sebelumnya, ada langkah penting yang diperlukan untuk menggunakan modul npm JavaScript modern. Penentuan dua konfigurasi babel-loader yang terpisah memungkinkan fitur bahasa modern yang ditemukan di node_modules hingga ES2017 dikompilasi secara otomatis, sekaligus tetap mentranspilasi kode pihak pertama Anda dengan plugin dan preset Babel yang ditentukan dalam konfigurasi project Anda. Cara ini tidak menghasilkan paket modern dan lama untuk penyiapan modul/nomodule, tetapi memungkinkan untuk menginstal dan menggunakan paket npm yang berisi JavaScript modern tanpa merusak browser lama.

webpack-plugin-modern-npm menggunakan teknik ini untuk mengompilasi dependensi npm yang memiliki kolom "exports" di package.json, karena kolom tersebut mungkin berisi sintaksis modern:

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

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

Atau, Anda dapat menerapkan teknik ini secara manual di konfigurasi webpack dengan memeriksa kolom "exports" di package.json modul saat di-resolve. Menghilangkan penyimpanan dalam cache agar lebih singkat, penerapan kustom mungkin terlihat seperti ini:

// 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'],
          },
        },
      },
    ],
  },
};

Saat menggunakan pendekatan ini, Anda harus memastikan sintaksis modern didukung oleh minifier Anda. Terser dan uglify-es memiliki opsi untuk menentukan {ecma: 2017} guna mempertahankan dan dalam beberapa kasus menghasilkan sintaksis ES2017 selama kompresi dan pemformatan.

Gabungan

Rollup memiliki dukungan bawaan untuk membuat beberapa kumpulan paket sebagai bagian dari satu build, dan menghasilkan kode modern secara default. Oleh karena itu, Rollup dapat dikonfigurasi untuk menghasilkan paket modern dan lama dengan plugin resmi yang mungkin sudah Anda gunakan.

@rollup/plugin-babel

Jika Anda menggunakan Rollup, metode getBabelOutputPlugin() (yang disediakan oleh plugin Babel resmi Rollup) akan mengubah kode dalam paket yang dihasilkan, bukan modul sumber individual. Rollup memiliki dukungan bawaan untuk membuat beberapa kumpulan paket sebagai bagian dari satu build, masing-masing dengan pluginnya sendiri. Anda dapat menggunakannya untuk menghasilkan paket yang berbeda untuk versi modern dan lama dengan meneruskan masing-masing paket melalui konfigurasi plugin output Baabel yang berbeda:

// 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'],
        }),
      ],
    },
  ],
};

Alat build tambahan

Rollup dan webpack sangat mudah dikonfigurasi, yang secara umum berarti setiap project harus memperbarui konfigurasinya untuk mengaktifkan sintaksis JavaScript modern dalam dependensi. Ada juga alat build dengan tingkat lebih tinggi yang mendukung konvensi dan setelan default pada konfigurasi, seperti Parcel, Snowpack, Vite, dan WMR. Sebagian besar alat ini mengasumsikan dependensi npm mungkin berisi sintaksis modern, dan akan mentranspilasinya ke level sintaksis yang sesuai saat membangun untuk produksi.

Selain plugin khusus untuk webpack dan Rollup, paket JavaScript modern dengan penggantian lama dapat ditambahkan ke project apa pun menggunakan devolution. Devolution adalah alat mandiri yang mengubah output dari sistem build untuk menghasilkan varian JavaScript lama, yang memungkinkan pemaketan dan transformasi mengasumsikan target output modern.