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.
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.
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.
Perilaku yang mendorong aplikasi ini dipisahkan menjadi vendor (yaitu, Preact dan Emotion) serta paket kode khusus aplikasi (atau "potongan", sebagaimana disebut oleh webpack):
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.
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.