Aplikasi web saat ini bisa menjadi cukup besar, terutama bagian JavaScript-nya. Pada pertengahan tahun 2018, HTTP Archive menempatkan ukuran transfer median JavaScript di perangkat seluler sekitar 350 KB. Dan ini baru ukuran transfer. JavaScript sering dikompresi saat dikirim melalui jaringan, yang berarti jumlah JavaScript sebenarnya jauh lebih banyak setelah didekompresi oleh browser. Hal ini penting untuk diperhatikan, karena sejauh menyangkut pemrosesan resource, kompresi tidak relevan. JavaScript yang didekompresi sebesar 900 KB tetap 900 KB untuk parser dan compiler, meskipun mungkin berukuran sekitar 300 KB saat dikompresi.
JavaScript adalah resource yang mahal untuk diproses. Tidak seperti gambar yang hanya menimbulkan waktu dekode yang relatif kecil setelah didownload, JavaScript harus diuraikan, dikompilasi, lalu dieksekusi. Byte demi byte, hal ini membuat JavaScript lebih mahal daripada jenis resource lainnya.
Meskipun peningkatan terus dilakukan untuk meningkatkan efisiensi mesin JavaScript, peningkatan performa JavaScript—seperti biasa—adalah tugas developer.
Untuk itu, ada teknik untuk meningkatkan performa JavaScript. Pemisahan kode adalah salah satu teknik yang meningkatkan performa dengan mempartisi JavaScript aplikasi menjadi beberapa bagian, dan menayangkan bagian tersebut hanya ke rute aplikasi yang membutuhkannya.
Meskipun teknik ini berfungsi, teknik ini tidak mengatasi masalah umum aplikasi yang banyak menggunakan JavaScript, yaitu penyertaan kode yang tidak pernah digunakan. Tree shaking mencoba menyelesaikan masalah ini.
Apa itu tree shaking?
Tree shaking adalah bentuk penghapusan kode mati. Istilah ini dipopulerkan oleh Rollup, tetapi konsep penghapusan kode tidak terpakai telah ada sejak lama. Konsep ini juga dapat ditemukan 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. Setiap node dalam hierarki mewakili dependensi yang menyediakan fungsi berbeda untuk aplikasi Anda. Di aplikasi modern, dependensi ini dimasukkan melalui pernyataan import statis seperti berikut:
// Import all the array utilities!
import arrayUtils from "array-utils";
Saat aplikasi masih baru—seperti bibit pohon—aplikasi tersebut mungkin memiliki sedikit dependensi. Selain itu, library ini menggunakan sebagian besar—jika tidak semua—dependensi yang Anda tambahkan. Namun, seiring perkembangan aplikasi, lebih banyak dependensi dapat ditambahkan. Selain itu, dependensi yang lebih lama tidak digunakan lagi, tetapi mungkin tidak dihapus dari codebase Anda. Hasil akhirnya adalah aplikasi dikirimkan 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 contoh sebelumnya adalah bahwa alih-alih mengimpor semuanya dari modul "array-utils"—yang bisa berupa banyak kode—contoh ini hanya mengimpor bagian tertentu dari modul tersebut. Dalam build dev, hal 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 membuat build produksi tersebut lebih kecil. Dalam panduan ini, Anda akan mempelajari cara melakukannya.
Menemukan peluang untuk menggoyangkan pohon
Untuk tujuan ilustrasi, aplikasi satu halaman contoh tersedia yang menunjukkan cara kerja tree shaking. Anda dapat meng-clone-nya dan mengikuti langkah-langkahnya jika mau, tetapi kita akan membahas setiap langkahnya bersama-sama dalam panduan ini, jadi meng-clone tidak diperlukan (kecuali jika Anda lebih suka belajar secara langsung).
Aplikasi contoh adalah database pedal efek gitar yang dapat ditelusuri. Anda memasukkan kueri dan daftar pedal efek akan muncul.
Perilaku yang mendorong aplikasi ini dibagi menjadi vendor (yaitu, Preact dan Emotion) serta paket kode khusus aplikasi (atau "chunk", sebagaimana sebutan webpack):
Bundle JavaScript yang ditampilkan pada gambar di atas adalah build produksi, yang berarti dioptimalkan melalui pengubahan nama. 21,1 KB untuk bundle khusus aplikasi tidak terlalu buruk, tetapi perlu diperhatikan bahwa tidak ada penghapusan kode yang tidak terpakai sama sekali. Mari kita lihat kode aplikasi dan lihat apa yang dapat dilakukan untuk memperbaikinya.
Dalam 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 yang seperti ini harus Anda perhatikan. Baris tertentu ini mengatakan "import semua dari modul utils, dan menempatkannya di namespace yang disebut utils." Pertanyaan besarnya di sini adalah, "berapa banyak hal yang ada dalam modul tersebut?"
Jika melihat kode sumber modul utils, Anda akan menemukan sekitar 1.300 baris kode.
Apakah Anda membutuhkan semua barang itu? Mari kita periksa ulang dengan menelusuri file komponen utama yang mengimpor modul utils untuk melihat berapa banyak instance namespace tersebut yang muncul.
utils yang telah kita impor banyak modulnya hanya dipanggil tiga kali dalam file komponen utama.
Ternyata, namespace utils hanya muncul di tiga tempat dalam aplikasi kita—tetapi untuk fungsi apa? Jika Anda melihat file komponen utama lagi, tampaknya hanya ada satu fungsi, yaitu utils.simpleSort, yang digunakan untuk mengurutkan daftar hasil penelusuran berdasarkan sejumlah kriteria saat 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 file 1.300 baris dengan banyak ekspor, hanya satu yang digunakan. Hal ini menyebabkan pengiriman banyak JavaScript yang tidak digunakan.
Meskipun aplikasi contoh ini sedikit 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 agar penghapusan kode yang tidak terpakai dapat bermanfaat, bagaimana cara melakukannya?
Mencegah Babel mentranspilasi modul ES6 ke modul CommonJS
Babel adalah alat yang sangat diperlukan, tetapi dapat membuat efek penghilangan kode yang tidak terpakai menjadi 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 tahu apa yang harus dihapus dari paket jika Anda memutuskan untuk menggunakannya. Solusinya adalah mengonfigurasi @babel/preset-env agar tidak memodifikasi modul ES6 secara eksplisit. Di mana pun Anda mengonfigurasi Babel—baik di babel.config.js maupun package.json—hal ini melibatkan penambahan sesuatu yang sedikit berbeda:
// babel.config.js
export default {
presets: [
[
"@babel/preset-env", {
modules: false
}
]
]
}
Menentukan modules: false dalam konfigurasi @babel/preset-env akan membuat Babel berperilaku seperti yang diinginkan, sehingga webpack dapat menganalisis pohon dependensi Anda dan menghapus dependensi yang tidak digunakan.
Memperhatikan efek samping
Aspek lain yang perlu dipertimbangkan saat menghapus dependensi yang tidak digunakan dari aplikasi Anda adalah apakah modul project Anda memiliki efek samping. Contoh efek samping adalah saat 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 memodifikasi array fruits, yang berada di luar cakupannya.
Efek samping juga berlaku untuk modul ES6, dan hal ini penting dalam konteks penghapusan kode yang tidak digunakan (tree shaking). Modul yang menerima input yang dapat diprediksi dan menghasilkan output yang sama-sama dapat diprediksi tanpa mengubah apa pun di luar cakupannya sendiri adalah dependensi yang dapat dihapus dengan aman jika kita tidak menggunakannya. Bagian kode ini bersifat mandiri dan modular. Oleh karena itu, "modul".
Untuk webpack, 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 yang tidak bebas dari efek samping:
{
"name": "webpack-tree-shaking-example",
"version": "1.0.0",
"sideEffects": [
"./src/utils/utils.js"
]
}
Pada contoh terakhir, file apa pun yang tidak ditentukan akan dianggap bebas dari efek samping. Jika tidak ingin menambahkannya ke file package.json, Anda juga dapat menentukan tanda ini di konfigurasi webpack melalui module.rules.
Mengimpor hanya yang diperlukan
Setelah menginstruksikan Babel untuk tidak mengubah modul ES6, sedikit penyesuaian pada sintaksis import diperlukan untuk hanya memanggil 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);
}
Ini seharusnya semua yang diperlukan agar tree shaking berfungsi dalam contoh ini. Berikut adalah output webpack sebelum memangkas pohon 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
Berikut adalah output setelah penghapusan kode yang tidak terpakai 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, paket main yang paling diuntungkan. Dengan menghapus bagian modul utils yang tidak digunakan, paket main akan berkurang sekitar 60%. Hal ini tidak hanya mengurangi waktu yang dibutuhkan skrip untuk mendownload, tetapi juga waktu pemrosesan.
Ayo goyangkan beberapa pohon!
Seberapa besar manfaat yang Anda peroleh dari penghapusan kode yang tidak terpakai bergantung pada aplikasi, dependensi, dan arsitekturnya. Cobalah! Jika Anda yakin belum menyiapkan bundler modul untuk melakukan pengoptimalan ini, tidak ada salahnya mencoba dan melihat manfaatnya bagi aplikasi Anda.
Anda mungkin mendapatkan peningkatan performa yang signifikan dari penghapusan kode yang tidak terpakai, atau tidak terlalu banyak. Namun, dengan mengonfigurasi sistem build untuk memanfaatkan pengoptimalan ini dalam build produksi dan mengimpor secara selektif hanya yang dibutuhkan aplikasi, Anda akan secara proaktif menjaga agar paket aplikasi tetap sekecil mungkin.
Terima kasih khusus kepada Kristofer Baxter, Jason Miller, Addy Osmani, Jeff Posnick, Sam Saccone, dan Philip Walton atas masukan berharga mereka, yang secara signifikan meningkatkan kualitas artikel ini.