Mengurangi payload JavaScript dengan tree shaking

Aplikasi web saat ini dapat menjadi cukup besar, terutama bagian JavaScript-nya. Per pertengahan 2018, Arsip HTTP menempatkan ukuran transfer median JavaScript di perangkat seluler sekitar 350 KB. Dan ini baru ukuran transfer. JavaScript sering kali dikompresi saat dikirim melalui jaringan, yang berarti jumlah JavaScript aktual akan jauh lebih banyak setelah browser mendekompresikannya. Hal ini penting untuk ditunjukkan, karena sehubungan dengan pemrosesan resource, kompresi tidak relevan. JavaScript 900 KB yang didekompresi masih berukuran 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 dikompresi menjadi 300 KB, skrip tersebut masih memiliki ukuran JavaScript sebesar 900 KB yang harus diuraikan, dikompilasi, dan dieksekusi.

JavaScript adalah resource yang mahal untuk diproses. Tidak seperti gambar yang hanya memerlukan waktu dekode yang relatif tidak penting setelah didownload, JavaScript harus diuraikan, dikompilasi, lalu dieksekusi. Byte demi byte, hal ini membuat JavaScript lebih mahal daripada jenis resource lainnya.

Diagram yang membandingkan waktu pemrosesan JavaScript 170 KB dengan gambar JPEG berukuran sama. Resource JavaScript jauh lebih banyak menggunakan resource byte demi byte daripada JPEG.
Biaya pemrosesan penguraian/kompilasi JavaScript 170 KB vs waktu dekode JPEG berukuran setara. (sumber).

Meskipun peningkatan terus dilakukan untuk meningkatkan efisiensi mesin JavaScript, meningkatkan performa JavaScript adalah—seperti biasa—tugas developer.

Untuk itu, ada teknik untuk meningkatkan performa JavaScript. Pembagian 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 berat JavaScript, yaitu penyertaan kode yang tidak pernah digunakan. Tree shaking mencoba mengatasi masalah ini.

Apa yang dimaksud dengan tree shaking?

Tree shaking adalah bentuk penghapusan kode mati. Istilah ini dipopulerkan oleh Rollup, tetapi konsep penghapusan kode mati telah ada selama beberapa waktu. Konsep ini juga telah diterapkan di webpack, yang ditunjukkan dalam artikel ini melalui aplikasi contoh.

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

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

Saat masih baru—seperti pohon muda—aplikasi mungkin memiliki sedikit dependensi. Aplikasi ini juga menggunakan sebagian besar—jika tidak semua—dependensi yang Anda tambahkan. Namun, seiring aplikasi Anda berkembang, lebih banyak dependensi dapat ditambahkan. Untuk memperburuk masalah, dependensi lama tidak digunakan lagi, tetapi mungkin tidak dihapus dari codebase Anda. Hasil akhirnya adalah aplikasi akhirnya 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, alih-alih mengimpor semuanya dari modul "array-utils"—yang mungkin berisi banyak kode—contoh ini hanya mengimpor bagian tertentu dari modul tersebut. Dalam build developer, 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 mengguncang pohon

Untuk tujuan ilustrasi, contoh aplikasi satu halaman tersedia yang menunjukkan cara kerja tree shaking. Anda dapat meng-clone-nya dan mengikutinya jika mau, tetapi kita akan membahas setiap langkahnya bersama-sama dalam panduan ini, sehingga cloning tidak diperlukan (kecuali jika 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 "chunk", seperti yang disebut webpack):

Screenshot dua paket kode aplikasi (atau bagian) 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 dioptimalkan melalui uglification. 21,1 KB untuk paket khusus aplikasi bukanlah ukuran yang buruk, tetapi perlu diperhatikan bahwa tidak ada tree shaking yang terjadi. Mari kita lihat kode aplikasi dan lihat apa yang dapat dilakukan untuk memperbaikinya.

Dalam aplikasi apa pun, menemukan peluang pembersihan hierarki 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 menyatakan "import semuanya dari modul utils, dan menempatkannya dalam namespace yang disebut utils". Pertanyaan besar yang harus diajukan 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 memerlukan semua hal tersebut? Mari kita periksa kembali dengan menelusuri file komponen utama yang mengimpor modul utils untuk melihat jumlah instance namespace tersebut yang muncul.

Screenshot penelusuran di editor teks untuk 'utils.', yang hanya menampilkan 3 hasil.
Namespace utils tempat kita mengimpor banyak modul 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 kembali file komponen utama, 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 memang agak dibuat-buat, hal ini tidak mengubah fakta bahwa jenis skenario sintetis ini menyerupai peluang pengoptimalan sebenarnya yang mungkin Anda temui di aplikasi web produksi. Setelah Anda mengidentifikasi peluang agar tree shaking berguna, bagaimana cara melakukannya?

Mencegah Babel mentranspile modul ES6 ke modul CommonJS

Babel adalah alat yang sangat diperlukan, tetapi dapat membuat efek shaking tree 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 dipangkas dari paket 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 maupun 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 akan membuat Babel berperilaku seperti yang diinginkan, yang memungkinkan webpack menganalisis hierarki dependensi Anda dan menghapus dependensi yang tidak digunakan.

Mempertimbangkan efek samping

Aspek lain yang perlu dipertimbangkan saat menghapus dependensi dari aplikasi adalah apakah modul project Anda memiliki efek samping. Contoh efek samping adalah saat fungsi mengubah sesuatu di luar cakupannya sendiri, yang merupakan efek samping 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 hal ini penting dalam konteks tree shaking. Modul yang mengambil 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. Ini adalah bagian kode modular yang berdiri sendiri. 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 efek samping:

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

Pada contoh kedua, setiap file 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.

Hanya mengimpor yang diperlukan

Setelah menginstruksikan Babel untuk tidak mengubah modul ES6, sedikit penyesuaian pada sintaksis import diperlukan untuk hanya memasukkan 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 adalah semua yang diperlukan agar tree shaking berfungsi dalam contoh ini. Ini adalah output webpack sebelum mengocok 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, paket main yang paling diuntungkan. Dengan menghapus bagian modul utils yang tidak digunakan, paket main akan menyusut sekitar 60%. Hal ini tidak hanya mengurangi jumlah waktu yang diperlukan skrip untuk mendownload, tetapi juga waktu pemrosesan.

Ayo goyang pohon!

Manfaat apa pun yang Anda dapatkan dari tree shaking bergantung pada aplikasi Anda serta dependensi dan arsitekturnya. Cobalah! Jika Anda tahu pasti bahwa Anda 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 tree shaking, atau tidak banyak sama sekali. Namun, dengan mengonfigurasi sistem build untuk memanfaatkan pengoptimalan ini dalam build produksi dan mengimpor secara selektif hanya yang diperlukan aplikasi, Anda akan secara proaktif menjaga paket aplikasi Anda 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.