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 keberadaan JavaScript lama tetap menjadi sumber masalah performa terbesar di web saat ini.

JavaScript Modern

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

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

Fitur dalam spesifikasi bahasa versi 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 merupakan mayoritas browser, tetapi tidak cukup aman untuk mengandalkan fitur tersebut secara langsung. Artinya, walaupun JavaScript "modern" adalah target yang terus berubah, ES2017 memiliki kompatibilitas browser dengan rentang terluas sekaligus menyertakan sebagian besar fitur sintaksis modern yang umum digunakan. Dengan kata lain, ES2017 adalah yang paling mendekati sintaksis modern saat ini.

JavaScript Versi Lama

JavaScript lama adalah kode yang secara khusus menghindari penggunaan semua fitur bahasa di atas. Sebagian besar developer menulis kode sumber mereka menggunakan sintaksis modern, tetapi mengompilasi semuanya ke sintaksis lama untuk meningkatkan dukungan browser. Mengompilasi ke sintaksis lama memang meningkatkan dukungan browser, tetapi efeknya 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 berukuran sekitar 20% lebih besar dan lebih lambat daripada kode modern yang setara. Kekurangan alat dan kesalahan konfigurasi sering kali memperlebar kesenjangan ini lebih jauh.

  • Library yang diinstal mencakup hingga 90% kode JavaScript produksi biasa. Kode library menimbulkan overhead JavaScript lama yang lebih tinggi 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" menyiratkan versi Node minimal 12.8, yang mendukung ES2019. Artinya, modul apa pun yang dirujuk menggunakan kolom "exports" dapat ditulis dalam JavaScript modern. Konsumen paket harus menganggap modul dengan kolom "exports" berisi kode modern dan melakukan transpilasi jika diperlukan.

Khusus modern

Jika Anda ingin memublikasikan paket dengan kode modern dan menyerahkannya kepada konsumen untuk menangani transpilasinya 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 pengoptimalan fallback lama dan bundler ESM

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

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

Banyak bundler, seperti webpack dan Rollup, mengandalkan kolom ini untuk memanfaatkan fitur modul dan mengaktifkan tree shaking. Ini masih merupakan paket lama yang tidak berisi kode modern selain sintaksis import/export, jadi gunakan pendekatan ini untuk mengirimkan kode modern dengan fallback lama yang masih dioptimalkan untuk bundling.

JavaScript modern dalam aplikasi

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

Dengan semakin banyaknya paket npm yang beralih ke JavaScript modern, penting untuk memastikan bahwa alat build disiapkan untuk menanganinya. Ada kemungkinan besar bahwa 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 mentranspile dependensi ke target sintaksis yang sama dengan kode sumber Anda.

webpack

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

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

Anda juga dapat mengonfigurasi webpack untuk menghasilkan paket yang dioptimalkan yang menghapus fungsi wrapper yang tidak diperlukan saat menargetkan lingkungan Modul ES modern. Tindakan ini juga 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 mengirimkan JavaScript modern sekaligus tetap mendukung browser lama, seperti Optimize Plugin dan BabelEsmPlugin.

Plugin Optimize

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

Karena Plugin Pengoptimalan beroperasi pada paket, bukan modul individual, plugin ini memproses kode aplikasi dan dependensi Anda secara merata. Hal ini membuat dependensi JavaScript modern dari npm aman digunakan, karena kodenya akan dipaketkan dan ditranspile ke sintaksis yang benar. Solusi ini juga dapat lebih cepat daripada solusi tradisional yang melibatkan dua langkah kompilasi, sambil tetap menghasilkan paket terpisah untuk browser modern dan lama. Kedua kumpulan paket dirancang untuk dimuat menggunakan pola module/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 memaketkan kode modern dan lama secara terpisah. Alat ini juga menangani pengoperasian Babel untuk Anda, dan meminimalkan paket menggunakan Terser dengan setelan optimal 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: mentranspil modul sumber dua kali versus mentranspil paket yang dihasilkan.

BabelEsmPlugin

BabelEsmPlugin adalah plugin webpack yang berfungsi bersama dengan @babel/preset-env untuk membuat versi modern dari paket yang ada untuk mengirimkan kode yang kurang ditranspil 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 berbagai 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 berintegrasi dengan lancar ke dalam konfigurasi webpack yang ada dan menjadikannya salah satu opsi paling praktis yang tersedia.

Mengonfigurasi babel-loader untuk mentranspile 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. Menentukan dua konfigurasi babel-loader terpisah memungkinkan kompilasi fitur bahasa modern yang ditemukan di node_modules ke ES2017 secara otomatis, sambil tetap mentranspilasi kode pihak pertama Anda sendiri dengan plugin dan preset Babel yang ditentukan dalam konfigurasi project Anda. Tindakan ini tidak membuat paket modern dan lama untuk penyiapan modul/nomodule, tetapi memungkinkan penginstalan dan penggunaan 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-nya, karena kolom ini 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 dalam konfigurasi webpack dengan memeriksa kolom "exports" di package.json modul saat di-resolve. Dengan menghapus penyimpanan dalam cache untuk mempersingkat, implementasi 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. Terser dan uglify-es memiliki opsi untuk menentukan {ecma: 2017} guna mempertahankan dan dalam beberapa kasus menghasilkan sintaksis ES2017 selama kompresi dan pemformatan.

Penggabungan

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

@rollup/plugin-babel

Jika Anda menggunakan Rollup, metode getBabelOutputPlugin() (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 modern dan lama dengan meneruskan setiap paket melalui konfigurasi plugin output Babel 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 dapat dikonfigurasi, yang umumnya berarti setiap project harus memperbarui konfigurasinya untuk mengaktifkan sintaksis JavaScript modern dalam dependensi. Ada juga alat build tingkat tinggi yang lebih menyukai konvensi dan default daripada konfigurasi, seperti Parcel, Snowpack, Vite, dan WMR. Sebagian besar alat ini menganggap dependensi npm dapat berisi sintaksis modern, dan akan mentranspilenya ke tingkat sintaksis yang sesuai saat mem-build untuk produksi.

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