Mengurangi payload JavaScript dengan tree shaking

Aplikasi web saat ini bisa menjadi sangat besar, terutama bagian JavaScript-nya. Sejak pertengahan 2018, Arsip HTTP menempatkan ukuran transfer median JavaScript di perangkat seluler sekitar 350 KB. Ini hanyalah ukuran transfer. JavaScript sering kali dikompresi saat dikirim melalui jaringan, yang berarti bahwa jumlah JavaScript yang sebenarnya sedikit lebih banyak setelah browser melakukan dekompresi. Hal ini penting untuk ditunjukkan, karena terkait dengan pemrosesan resource, kompresi sudah tidak relevan. 900 KB JavaScript yang didekompresi tetap berukuran 900 KB untuk parser dan compiler, meskipun mungkin sekitar 300 KB saat dikompresi.

Diagram yang menggambarkan proses mendownload, mendekompresi, mengurai, mengompilasi, dan mengeksekusi JavaScript.
Proses mendownload dan menjalankan JavaScript. Perhatikan bahwa meskipun ukuran transfer skrip dikompresi sebesar 300 KB, JavaScript masih harus diuraikan, dikompilasi, dan dieksekusi sebesar 900 KB.

JavaScript adalah resource yang mahal untuk diproses. Tidak seperti gambar yang hanya menimbulkan waktu dekode yang relatif sepele setelah didownload, JavaScript harus diurai, dikompilasi, lalu akhirnya dieksekusi. Byte untuk byte, hal ini membuat JavaScript lebih mahal daripada jenis resource lainnya.

Diagram yang membandingkan waktu pemrosesan JavaScript 170 KB versus gambar JPEG berukuran setara. Resource JavaScript merupakan byte yang membutuhkan banyak resource untuk byte yang jauh lebih banyak dibandingkan JPEG.
Biaya pemrosesan penguraian/kompilasi JavaScript 170 KB vs waktu dekode dari JPEG yang berukuran setara. (sumber).

Sementara peningkatan terus dilakukan untuk meningkatkan efisiensi mesin JavaScript, meningkatkan performa JavaScript adalah tugas bagi developer.

Untuk itu, ada teknik untuk meningkatkan kinerja JavaScript. Pemisahan kode adalah salah satu teknik yang meningkatkan performa dengan mempartisi JavaScript aplikasi menjadi beberapa potongan, dan menyajikan potongan tersebut hanya ke rute aplikasi yang membutuhkannya.

Meskipun teknik ini berhasil, teknik ini tidak mengatasi masalah umum pada aplikasi yang sarat JavaScript, yaitu menyertakan kode yang tidak pernah digunakan. Tree shaking mencoba memecahkan masalah ini.

Apa itu tree shaking?

Tree shaking adalah bentuk penghilangan kode yang mati. Istilah ini dipopulerkan oleh Rollup, tetapi konsep penghapusan kode yang mati telah ada sejak lama. Konsep ini juga telah menemukan pembelian di webpack, yang ditunjukkan dalam artikel ini melalui aplikasi contoh.

Istilah "tree shaking" berasal dari model mental aplikasi Anda dan dependensinya sebagai struktur seperti pohon. Tiap node dalam hierarki mewakili dependensi yang menyediakan fungsi berbeda untuk aplikasi Anda. Dalam aplikasi modern, dependensi ini dibawa melalui pernyataan import statis seperti:

// Import all the array utilities!
import arrayUtils from "array-utils";

Saat aplikasi masih muda—papan, jika Anda mau—aplikasi mungkin hanya memiliki sedikit dependensi. AD juga menggunakan sebagian besar—atau bahkan tidak semua—dependensi yang Anda tambahkan. Namun, saat aplikasi Anda berkembang, lebih banyak dependensi dapat ditambahkan. Untuk memperparah masalah, dependensi yang lebih lama tidak dapat digunakan, tetapi mungkin tidak akan dipangkas dari codebase Anda. Hasil akhirnya adalah aplikasi akhirnya dikirim dengan banyak JavaScript yang tidak digunakan. Tree shaking mengatasi hal ini dengan memanfaatkan cara pernyataan import statis menarik bagian tertentu dari modul ES6:

// Import only some of the utilities!
import { unique, implode, explode } from "array-utils";

Perbedaan antara contoh import ini dan yang sebelumnya adalah, alih-alih mengimpor semuanya dari modul "array-utils"—yang mungkin akan membutuhkan banyak kode)—contoh ini hanya mengimpor bagian tertentu darinya. Dalam build dev, hal ini tidak mengubah apa pun, karena seluruh modul tetap diimpor. Di build produksi, webpack dapat dikonfigurasi untuk "menggoyangkan" ekspor dari modul ES6 yang tidak diimpor secara eksplisit, sehingga build produksi tersebut menjadi lebih kecil. Dalam panduan ini, Anda akan mempelajari cara melakukannya.

Menemukan peluang untuk menggoyangkan pohon

Untuk tujuan ilustrasi, tersedia aplikasi contoh satu halaman yang menunjukkan cara kerja tree shaking. Anda dapat membuat clone dan mengikutinya jika mau, tetapi kami akan membahas setiap langkahnya bersama-sama dalam panduan ini, jadi cloning tidak diperlukan (kecuali jika Anda suka belajar langsung).

Aplikasi contoh adalah database pedal efek gitar yang dapat ditelusuri. Anda memasukkan kueri dan daftar efek pedal akan muncul.

Screenshot contoh aplikasi satu halaman untuk menelusuri database pedal efek gitar.
Screenshot aplikasi contoh.

Perilaku yang mendorong aplikasi ini dipisahkan menjadi vendor (yaitu, Preak dan Emosi) serta paket kode khusus aplikasi (atau "bagian", seperti yang dipanggil webpack):

Screenshot dua paket kode aplikasi (atau potongan) yang ditampilkan di panel jaringan DevTools Chrome.
Dua paket JavaScript aplikasi. Ini adalah ukuran yang tidak dikompresi.

Paket JavaScript yang ditampilkan dalam gambar di atas adalah build produksi, yang berarti paket tersebut dioptimalkan melalui uglification. 21,1 KB untuk paket khusus aplikasi tidak buruk, tetapi perlu diperhatikan bahwa tidak ada guncangan pohon yang terjadi. Mari kita lihat kode aplikasi dan lihat apa yang dapat dilakukan untuk memperbaikinya.

Di aplikasi apa pun, menemukan peluang tree shaking akan melibatkan pencarian pernyataan import statis. Di dekat bagian atas file komponen utama, Anda akan melihat baris seperti ini:

import * as utils from "../../utils/utils";

Anda dapat mengimpor modul ES6 dengan berbagai cara, tetapi cara seperti ini harus menarik perhatian Anda. Baris khusus ini bertuliskan "import semuanya dari modul utils, dan memasukkannya ke dalam namespace bernama utils. Pertanyaan besar yang harus diajukan di sini adalah, "seberapa banyak hal-hal yang ada dalam modul itu?"

Jika melihat kode sumber modul utils, Anda akan melihat ada sekitar 1.300 baris kode.

Apakah Anda membutuhkan semua hal tersebut? Mari periksa kembali dengan menelusuri file komponen utama yang mengimpor modul utils untuk melihat berapa banyak instance namespace yang muncul.

Screenshot penelusuran di editor teks untuk 'utils', hanya menampilkan 3 hasil.
Namespace utils yang telah mengimpor banyak modul hanya dipanggil tiga kali dalam file komponen utama.

Ternyata, namespace utils hanya muncul di tiga tempat dalam aplikasi, tetapi untuk fungsi apa? Jika Anda melihat lagi file komponen utama, tampaknya hanya satu fungsi, yaitu utils.simpleSort, yang digunakan untuk mengurutkan daftar hasil penelusuran berdasarkan sejumlah kriteria ketika menu dropdown pengurutan diubah:

if (this.state.sortBy === "model") {
  // `simpleSort` gets used here...
  json = utils.simpleSort(json, "model", this.state.sortOrder);
} else if (this.state.sortBy === "type") {
  // ..and here...
  json = utils.simpleSort(json, "type", this.state.sortOrder);
} else {
  // ..and here.
  json = utils.simpleSort(json, "manufacturer", this.state.sortOrder);
}

Dari 1.300 file baris dengan banyak ekspor, hanya satu yang digunakan. Ini menyebabkan pengiriman banyak JavaScript yang tidak terpakai.

Meskipun aplikasi contoh ini memang dibuat-buat, hal ini tidak mengubah fakta bahwa skenario sintetis semacam ini menyerupai peluang pengoptimalan sebenarnya yang mungkin Anda temui di aplikasi web produksi. Setelah Anda mengidentifikasi peluang tree shaking berguna, bagaimana cara melakukannya?

Menjaga Babel agar tidak mentranspilasi modul ES6 ke modul CommonJS

Babel adalah alat yang sangat diperlukan, tetapi dapat membuat efek guncangan pohon sedikit lebih sulit diamati. Jika Anda menggunakan @babel/preset-env, Babel dapat mengubah modul ES6 menjadi modul CommonJS yang lebih kompatibel secara luas—yaitu, modul yang Anda require, bukan import.

Karena tree shaking lebih sulit dilakukan untuk modul CommonJS, webpack tidak akan mengetahui apa yang harus dipangkas dari bundle jika Anda memutuskan untuk menggunakannya. Solusinya adalah mengonfigurasi @babel/preset-env untuk secara eksplisit membiarkan modul ES6. Di mana pun Anda mengonfigurasi Babel—baik di babel.config.js atau package.json—hal ini melibatkan penambahan sedikit hal tambahan:

// babel.config.js
export default {
  presets: [
    [
      "@babel/preset-env", {
        modules: false
      }
    ]
  ]
}

Menentukan modules: false dalam konfigurasi @babel/preset-env membuat Babel berperilaku seperti yang diinginkan, yang memungkinkan webpack menganalisis hierarki dependensi dan menghilangkan dependensi yang tidak digunakan.

Mempertimbangkan efek samping

Aspek lain yang perlu dipertimbangkan saat menggoyangkan dependensi dari aplikasi adalah apakah modul proyek Anda memiliki efek samping. Contoh efek samping adalah ketika fungsi memodifikasi sesuatu di luar cakupannya, yang merupakan efek samping dari eksekusinya:

let fruits = ["apple", "orange", "pear"];

console.log(fruits); // (3) ["apple", "orange", "pear"]

const addFruit = function(fruit) {
  fruits.push(fruit);
};

addFruit("kiwi");

console.log(fruits); // (4) ["apple", "orange", "pear", "kiwi"]

Dalam contoh ini, addFruit menghasilkan efek samping saat memodifikasi array fruits, yang berada di luar cakupannya.

Efek samping juga berlaku untuk modul ES6, dan hal ini penting dalam konteks tree shaking. Modul yang menggunakan input yang dapat diprediksi dan menghasilkan output yang dapat diprediksi serta menghasilkan output yang sama-sama dapat diprediksi tanpa mengubah apa pun di luar cakupannya adalah dependensi yang dapat dihapus dengan aman jika kita tidak menggunakannya. Kode ini bersifat mandiri dan modular. Oleh karena itu, "modul".

Jika terkait dengan webpack, petunjuk dapat digunakan untuk menentukan bahwa paket dan dependensinya bebas dari efek samping dengan menetapkan "sideEffects": false dalam file package.json project:

{
  "name": "webpack-tree-shaking-example",
  "version": "1.0.0",
  "sideEffects": false
}

Atau, Anda dapat memberi tahu webpack tentang file tertentu yang tidak bebas efek samping:

{
  "name": "webpack-tree-shaking-example",
  "version": "1.0.0",
  "sideEffects": [
    "./src/utils/utils.js"
  ]
}

Pada contoh yang terakhir, setiap file yang tidak ditentukan akan dianggap bebas efek samping. Jika tidak ingin menambahkan ini ke file package.json, Anda juga dapat menentukan tanda ini dalam konfigurasi webpack melalui module.rules.

Mengimpor yang diperlukan saja

Setelah memerintahkan Babel untuk membiarkan modul ES6 sendirian, sedikit penyesuaian pada sintaksis import diperlukan untuk hanya menyertakan fungsi yang diperlukan dari modul utils. Dalam contoh panduan ini, yang diperlukan hanyalah fungsi simpleSort:

import { simpleSort } from "../../utils/utils";

Karena hanya simpleSort yang diimpor, bukan seluruh modul utils, setiap instance utils.simpleSort harus diubah menjadi simpleSort:

if (this.state.sortBy === "model") {
  json = simpleSort(json, "model", this.state.sortOrder);
} else if (this.state.sortBy === "type") {
  json = simpleSort(json, "type", this.state.sortOrder);
} else {
  json = simpleSort(json, "manufacturer", this.state.sortOrder);
}

Seharusnya inilah yang diperlukan agar tree shaking berfungsi dalam contoh ini. Ini adalah output webpack sebelum menggoyangkan hierarki dependensi:

                 Asset      Size  Chunks             Chunk Names
js/vendors.16262743.js  37.1 KiB       0  [emitted]  vendors
   js/main.797ebb8b.js  20.8 KiB       1  [emitted]  main

Ini adalah output setelah tree shaking berhasil:

                 Asset      Size  Chunks             Chunk Names
js/vendors.45ce9b64.js  36.9 KiB       0  [emitted]  vendors
   js/main.559652be.js  8.46 KiB       1  [emitted]  main

Meskipun kedua paket menyusut, sebenarnya paket main tersebut yang paling diuntungkan. Dengan menghilangkan bagian modul utils yang tidak digunakan, paket main akan menyusut sekitar 60%. Hal ini tidak hanya mengurangi jumlah waktu yang dibutuhkan skrip untuk mendownload, tetapi juga waktu pemrosesan.

Goyangkan beberapa pohon!

Apa pun jarak tempuh yang Anda peroleh dari tree shaking bergantung pada aplikasi Anda serta dependensi dan arsitekturnya. Cobalah! Jika mengetahui fakta bahwa Anda belum menyiapkan pemaket modul untuk melakukan pengoptimalan ini, tidak ada salahnya mencoba dan melihat bagaimana manfaatnya bagi aplikasi Anda.

Anda mungkin menyadari peningkatan performa yang signifikan dari tree shaking, atau tidak banyak sama sekali. Namun, dengan mengonfigurasi sistem build untuk memanfaatkan pengoptimalan dalam build produksi ini dan hanya mengimpor yang dibutuhkan aplikasi secara selektif, Anda akan secara proaktif menjaga paket aplikasi sekecil mungkin.

Terima kasih banyak kepada Kristofer Baxter, Jason Miller, Addy Osmani, Jeff Posnick, Sam Saccone, dan Philip Walton atas masukan mereka yang berharga, yang secara signifikan meningkatkan kualitas artikel ini.