Mengurangi payload JavaScript dengan tree shaking

Aplikasi web masa kini bisa menjadi sangat besar, terutama bagian JavaScript darinya. Sejak pertengahan 2018, Arsip HTTP menempatkan ukuran transfer median JavaScript di perangkat seluler sekitar 350 KB. Ini hanyalah ukuran transfer. JavaScript sering dikompresi saat dikirim melalui jaringan, yang berarti bahwa jumlah JavaScript sebenarnya sedikit lebih banyak setelah browser mendekompresinya. Hal tersebut penting untuk diperhatikan karena selama pemrosesan resource berkaitan, kompresi tidak relevan. 900 KB JavaScript yang didekompresi masih sebesar 900 KB untuk parser dan compiler, meskipun mungkin berukuran 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 adalah 300 KB yang dikompresi, JavaScript tetap bernilai 900 KB yang harus diurai, dikompilasi, dan dieksekusi.

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

Diagram yang membandingkan waktu pemrosesan JavaScript sebesar 170 KB dengan gambar JPEG yang berukuran setara. Sumber daya JavaScript adalah byte yang membutuhkan sumber daya untuk byte jauh lebih banyak daripada JPEG.
Biaya pemrosesan mengurai/mengompilasi 170 KB JavaScript vs waktu dekode JPEG yang berukuran setara. (sumber).

Meskipun peningkatan terus-menerus dilakukan untuk meningkatkan efisiensi mesin JavaScript, meningkatkan performa JavaScript menjadi tugas developer yang selalu ditingkatkan.

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

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

Apa itu guncangan pohon?

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

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

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

Saat masih muda—anak pohon, jika Anda masih muda—aplikasi tersebut mungkin memiliki sedikit dependensi. Ia juga menggunakan sebagian besar—atau bahkan semua—dependensi yang Anda tambahkan. Namun, seiring aplikasi Anda berkembang pesat, semakin banyak dependensi dapat ditambahkan. Masalahnya akan semakin rumit, dependensi yang lebih lama tidak digunakan lagi, tetapi mungkin tidak dipangkas dari codebase Anda. Hasil akhirnya adalah aplikasi menghasilkan banyak JavaScript yang tidak digunakan. Tree shaking mengatasi masalah ini dengan memanfaatkan cara pernyataan import statis menarik bagian tertentu 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 bisa jadi berisi banyak kode)—contoh ini hanya mengimpor bagian tertentu darinya. Dalam build dev, ini tidak mengubah apa pun, karena seluruh modul akan diimpor. Dalam build produksi, webpack dapat dikonfigurasi untuk "menghapus" 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 kesempatan untuk menggoyangkan pohon

Untuk tujuan ilustrasi, tersedia contoh aplikasi satu halaman yang menunjukkan cara kerja tree shaking. Anda dapat membuat clone dan mengikutinya jika mau, tetapi kami akan membahas setiap langkahnya dalam panduan ini, sehingga cloning tidak perlu dilakukan (kecuali Anda menyukai pembelajaran langsung).

Aplikasi contoh adalah database pedal efek gitar yang dapat ditelusuri. Anda memasukkan kueri dan daftar pedal efek 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, Preact dan Emotion) serta paket kode khusus aplikasi (atau "potongan", sebagaimana disebut oleh 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 uglifikasi. 21,1 KB untuk paket khusus aplikasi bukanlah hal yang buruk, tetapi perlu diperhatikan bahwa tree shaking tidak terjadi apa pun. Mari kita lihat kode aplikasi dan lihat apa yang dapat dilakukan untuk memperbaikinya.

Dalam aplikasi apa pun, menemukan peluang tree shaking 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 modul seperti ini akan menarik perhatian Anda. Baris khusus ini mengatakan "import semuanya dari modul utils, dan memasukkannya ke dalam namespace bernama utils." Pertanyaan besar yang perlu diajukan di sini adalah, "berapa banyak hal-hal yang ada dalam modul itu?"

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

Apakah Anda memerlukan semua hal itu? Mari kita 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', yang hanya menampilkan 3 hasil.
Namespace utils yang menjadi asal dari banyak modul yang kami impor 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 kembali file komponen utama, file tersebut akan terlihat hanya satu fungsi, yaitu utils.simpleSort, yang digunakan untuk mengurutkan daftar hasil penelusuran berdasarkan sejumlah kriteria saat 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. Hal ini menyebabkan pengiriman banyak JavaScript yang tidak digunakan.

Meskipun aplikasi contoh ini diakui dibuat-buat, aplikasi contoh 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 manfaat tree shaking, bagaimana cara kerjanya?

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 gunakan dalam require, bukan import.

Karena tree shaking lebih sulit dilakukan untuk modul CommonJS, webpack tidak akan tahu apa yang harus dipangkas dari paket jika Anda memutuskan untuk menggunakannya. Solusinya adalah mengonfigurasi @babel/preset-env untuk membiarkan modul ES6 saja secara eksplisit. Di mana pun Anda mengonfigurasi Babel—baik di babel.config.js maupun package.json—langkah ini memerlukan penambahan sedikit 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 project Anda memiliki efek samping atau tidak. Contoh efek samping adalah saat suatu fungsi mengubah sesuatu di luar cakupannya sendiri, 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 mengubah array fruits, yang berada di luar cakupannya.

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

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

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

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

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

Pada contoh yang kedua, file apa pun yang tidak ditentukan akan dianggap bebas dari efek samping. Jika tidak ingin menambahkannya ke file package.json, Anda juga dapat menentukan flag ini dalam konfigurasi webpack melalui module.rules.

Mengimpor hanya hal-hal yang diperlukan

Setelah menginstruksikan Babel untuk membiarkan modul ES6 saja, diperlukan sedikit penyesuaian pada sintaksis import untuk memasukkan fungsi yang diperlukan saja 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 perlu 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 itulah yang diperlukan agar tree shaking bekerja 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, yang paling diuntungkan adalah paket main. Dengan menghilangkan bagian modul utils yang tidak digunakan, paket main akan menyusut sekitar 60%. Hal ini tidak hanya mengurangi jumlah waktu yang diperlukan skrip untuk mengunduh, tetapi juga waktu pemrosesan.

Goyangkan beberapa pohon!

Berapa pun jarak tempuh yang Anda dapatkan dari tree shaking bergantung pada aplikasi Anda, dependensi, dan arsitekturnya. Cobalah! Jika Anda sebenarnya belum menyiapkan pemaket modul untuk melakukan pengoptimalan ini, tidak ada salahnya mencoba dan melihat bagaimana hal itu menguntungkan 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 ini dalam build produksi dan secara selektif hanya mengimpor apa yang diperlukan aplikasi, 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.