Mengguncang Pohon: Panduan Referensi

Diterbitkan: 2022-03-10
Ringkasan cepat “Tree-shaking” adalah pengoptimalan kinerja yang harus dimiliki saat menggabungkan JavaScript. Dalam artikel ini, kami menyelam lebih dalam tentang cara kerjanya dan bagaimana spesifikasi dan praktik saling terkait untuk membuat bundel lebih ramping dan lebih berperforma. Plus, Anda akan mendapatkan daftar periksa goyangan pohon yang akan digunakan untuk proyek Anda.

Sebelum memulai perjalanan kita untuk mempelajari apa itu tree-shaking dan bagaimana mempersiapkan diri untuk sukses dengannya, kita perlu memahami modul apa yang ada di ekosistem JavaScript.

Sejak awal, program JavaScript telah berkembang dalam kompleksitas dan jumlah tugas yang mereka lakukan. Kebutuhan untuk mengelompokkan tugas-tugas seperti itu ke dalam lingkup eksekusi yang tertutup menjadi jelas. Kompartemen tugas, atau nilai ini, adalah apa yang kita sebut modul . Tujuan utamanya adalah untuk mencegah pengulangan dan meningkatkan kegunaan kembali. Jadi, arsitektur dirancang untuk memungkinkan jenis ruang lingkup khusus seperti itu, untuk mengekspos nilai dan tugas mereka, dan untuk mengkonsumsi nilai dan tugas eksternal.

Untuk mempelajari lebih dalam modul apa dan bagaimana cara kerjanya, saya merekomendasikan "Modul ES: Menyelam Mendalam Kartun". Tetapi untuk memahami nuansa tree-shaking dan konsumsi modul, definisi di atas sudah cukup.

Apa Arti Mengguncang Pohon Sebenarnya?

Sederhananya, tree-shaking berarti menghapus kode yang tidak dapat dijangkau (juga dikenal sebagai kode mati) dari sebuah bundel. Seperti yang dinyatakan oleh dokumentasi Webpack versi 3:

“Anda dapat membayangkan aplikasi Anda sebagai pohon. Kode sumber dan pustaka yang sebenarnya Anda gunakan mewakili daun pohon yang hijau dan hidup. Kode mati mewakili daun pohon yang berwarna coklat dan mati yang dikonsumsi oleh musim gugur. Untuk menyingkirkan daun-daun yang mati, Anda harus mengguncang pohon itu, menyebabkannya tumbang.”

Istilah ini pertama kali dipopulerkan di komunitas front-end oleh tim Rollup. Tetapi penulis dari semua bahasa dinamis telah berjuang dengan masalah ini sejak jauh sebelumnya. Ide algoritma tree-shaking dapat ditelusuri kembali setidaknya pada awal 1990-an.

Di tanah JavaScript, pengguncangan pohon telah dimungkinkan sejak spesifikasi modul ECMAScript (ESM) di ES2015, yang sebelumnya dikenal sebagai ES6. Sejak itu, pengocokan pohon telah diaktifkan secara default di sebagian besar bundler karena mereka mengurangi ukuran keluaran tanpa mengubah perilaku program.

Alasan utama untuk ini adalah bahwa ESM pada dasarnya statis. Mari kita membedah apa artinya.

Lebih banyak setelah melompat! Lanjutkan membaca di bawah ini

Modul ES vs. CommonJS

CommonJS mendahului spesifikasi ESM beberapa tahun. Itu muncul untuk mengatasi kurangnya dukungan untuk modul yang dapat digunakan kembali di ekosistem JavaScript. CommonJS memiliki fungsi require() yang mengambil modul eksternal berdasarkan jalur yang disediakan, dan menambahkannya ke ruang lingkup selama runtime.

Yang require adalah function seperti yang lain dalam suatu program membuatnya cukup sulit untuk mengevaluasi hasil panggilannya pada waktu kompilasi. Selain itu adalah fakta bahwa menambahkan panggilan require di mana saja dalam kode dimungkinkan — dibungkus dengan panggilan fungsi lain, dalam pernyataan if/else, dalam pernyataan sakelar, dll.

Dengan pembelajaran dan perjuangan yang dihasilkan dari adopsi arsitektur CommonJS secara luas, spesifikasi ESM telah ditetapkan pada arsitektur baru ini, di mana modul diimpor dan diekspor oleh masing-masing kata kunci import dan export . Oleh karena itu, tidak ada lagi panggilan fungsional. ESM juga diperbolehkan hanya sebagai deklarasi tingkat atas — menyarangkannya dalam struktur lain tidak dimungkinkan, karena statis : ESM tidak bergantung pada eksekusi runtime.

Ruang Lingkup dan Efek Samping

Namun, ada rintangan lain yang harus diatasi dengan menggoncang pohon untuk menghindari kembung: efek samping. Suatu fungsi dianggap memiliki efek samping ketika mengubah atau bergantung pada faktor-faktor di luar lingkup eksekusi. Sebuah fungsi dengan efek samping dianggap tidak murni . Fungsi murni akan selalu menghasilkan hasil yang sama, terlepas dari konteks atau lingkungan tempat fungsi itu dijalankan.

 const pure = (a:number, b:number) => a + b const impure = (c:number) => window.foo.number + c

Bundler melayani tujuannya dengan mengevaluasi kode yang diberikan sebanyak mungkin untuk menentukan apakah suatu modul murni. Tetapi evaluasi kode selama waktu kompilasi atau waktu bundling hanya bisa sejauh ini. Oleh karena itu, diasumsikan bahwa paket dengan efek samping tidak dapat dihilangkan dengan benar, bahkan ketika sama sekali tidak terjangkau.

Karena itu, bundler sekarang menerima kunci di dalam file package.json modul yang memungkinkan pengembang untuk mendeklarasikan apakah suatu modul tidak memiliki efek samping. Dengan cara ini, pengembang dapat memilih keluar dari evaluasi kode dan memberi petunjuk kepada bundler; kode dalam paket tertentu dapat dihilangkan jika tidak ada impor yang dapat dijangkau atau pernyataan require yang menautkannya. Ini tidak hanya membuat bundel yang lebih ramping, tetapi juga dapat mempercepat waktu kompilasi.

 { "name": "my-package", "sideEffects": false }

Jadi, jika Anda seorang pengembang paket, manfaatkan sideEffects dengan cermat sebelum menerbitkan, dan, tentu saja, revisi pada setiap rilis untuk menghindari perubahan yang tidak diharapkan.

Selain kunci root sideEffects , juga dimungkinkan untuk menentukan kemurnian berdasarkan file per file, dengan memberi anotasi pada komentar sebaris, /*@__PURE__*/ , ke pemanggilan metode Anda.

 const x = */@__PURE__*/eliminated_if_not_called()

Saya menganggap anotasi sebaris ini sebagai jalan keluar bagi pengembang konsumen, yang harus dilakukan jika sebuah paket belum menyatakan sideEffects: false atau jika perpustakaan memang menghadirkan efek samping pada metode tertentu.

Mengoptimalkan Webpack

Dari versi 4 dan seterusnya, Webpack memerlukan konfigurasi yang semakin sedikit untuk menjalankan praktik terbaik. Fungsionalitas untuk beberapa plugin telah dimasukkan ke dalam inti. Dan karena tim pengembangan menganggap ukuran bundel dengan sangat serius, mereka membuat pengocokan pohon menjadi mudah.

Jika Anda tidak terlalu suka mengotak-atik atau jika aplikasi Anda tidak memiliki kasus khusus, maka menggoyahkan dependensi Anda hanyalah masalah satu baris.

File webpack.config.js memiliki properti root bernama mode . Setiap kali nilai properti ini adalah production , itu akan menggoyang pohon dan mengoptimalkan modul Anda sepenuhnya. Selain menghilangkan kode mati dengan TerserPlugin , mode: 'production' akan mengaktifkan nama rusak deterministik untuk modul dan potongan, dan itu akan mengaktifkan plugin berikut:

  • penggunaan ketergantungan bendera,
  • bendera termasuk potongan,
  • rangkaian modul,
  • tidak memancarkan kesalahan.

Bukan kebetulan bahwa nilai pemicunya adalah production . Anda tidak ingin dependensi Anda dioptimalkan sepenuhnya dalam lingkungan pengembangan karena itu akan membuat masalah jauh lebih sulit untuk di-debug. Jadi saya akan menyarankan untuk melakukannya dengan salah satu dari dua pendekatan.

Di satu sisi, Anda dapat meneruskan tanda mode ke antarmuka baris perintah Webpack:

 # This will override the setting in your webpack.config.js webpack --mode=production

Atau, Anda dapat menggunakan variabel process.env.NODE_ENV di webpack.config.js :

 mode: process.env.NODE_ENV === 'production' ? 'production' : development

Dalam hal ini, Anda harus ingat untuk meneruskan --NODE_ENV=production dalam pipeline penerapan Anda.

Kedua pendekatan tersebut merupakan abstraksi di atas definePlugin yang banyak dikenal dari Webpack versi 3 dan di bawahnya. Opsi mana yang Anda pilih sama sekali tidak ada bedanya.

Webpack Versi 3 dan Di Bawah

Perlu disebutkan bahwa skenario dan contoh di bagian ini mungkin tidak berlaku untuk versi terbaru Webpack dan bundler lainnya. Bagian ini mempertimbangkan penggunaan UglifyJS versi 2, bukan Terser. UglifyJS adalah paket dari mana Terser bercabang, jadi evaluasi kode mungkin berbeda di antara mereka.

Karena Webpack versi 3 dan di bawahnya tidak mendukung properti sideEffects di package.json , semua paket harus dievaluasi sepenuhnya sebelum kode dihilangkan. Ini saja membuat pendekatan kurang efektif, tetapi beberapa peringatan harus dipertimbangkan juga.

Seperti disebutkan di atas, kompiler tidak memiliki cara untuk mengetahui dengan sendirinya ketika sebuah paket merusak lingkup global. Tapi itu bukan satu-satunya situasi di mana ia tidak menggoyahkan pohon. Ada skenario yang lebih kabur.

Ambil contoh paket ini dari dokumentasi Webpack:

 // transform.js import * as mylib from 'mylib'; export const someVar = mylib.transform({ // ... }); export const someOtherVar = mylib.transform({ // ... });

Dan inilah titik masuk dari bundel konsumen:

 // index.js import { someVar } from './transforms.js'; // Use `someVar`...

Tidak ada cara untuk menentukan apakah mylib.transform memicu efek samping. Oleh karena itu, tidak ada kode yang akan dihilangkan.

Berikut adalah situasi lain dengan hasil serupa:

  • menjalankan fungsi dari modul pihak ketiga yang tidak dapat diperiksa oleh kompiler,
  • mengekspor kembali fungsi yang diimpor dari modul pihak ketiga.

Alat yang mungkin membantu kompilator menjalankan pengocokan pohon adalah babel-plugin-transform-imports. Ini akan membagi semua anggota dan ekspor bernama menjadi ekspor default, memungkinkan modul untuk dievaluasi secara individual.

 // before transformation import { Row, Grid as MyGrid } from 'react-bootstrap'; import { merge } from 'lodash'; // after transformation import Row from 'react-bootstrap/lib/Row'; import MyGrid from 'react-bootstrap/lib/Grid'; import merge from 'lodash/merge';

Ini juga memiliki properti konfigurasi yang memperingatkan pengembang untuk menghindari pernyataan impor yang merepotkan. Jika Anda menggunakan Webpack versi 3 atau lebih tinggi, dan Anda telah melakukan uji tuntas dengan konfigurasi dasar dan menambahkan plugin yang disarankan, tetapi bundel Anda masih terlihat membengkak, maka saya sarankan untuk mencoba paket ini.

Pengangkatan Lingkup dan Waktu Kompilasi

Pada masa CommonJS, sebagian besar bundler hanya akan membungkus setiap modul dalam deklarasi fungsi lain dan memetakannya di dalam suatu objek. Itu tidak berbeda dari objek peta mana pun di luar sana:

 (function (modulesMap, entry) { // provided CommonJS runtime })({ "index.js": function (require, module, exports) { let { foo } = require('./foo.js') foo.doStuff() }, "foo.js": function(require, module, exports) { module.exports.foo = { doStuff: () => { console.log('I am foo') } } } }, "index.js")

Selain sulit untuk dianalisis secara statis, ini pada dasarnya tidak kompatibel dengan ESM, karena kami telah melihat bahwa kami tidak dapat membungkus pernyataan import dan export . Jadi, saat ini, bundler mengangkat setiap modul ke level teratas:

 // moduleA.js let $moduleA$export$doStuff = () => ({ doStuff: () => {} }) // index.js $moduleA$export$doStuff()

Pendekatan ini sepenuhnya kompatibel dengan ESM; plus, ini memungkinkan evaluasi kode untuk dengan mudah menemukan modul yang tidak dipanggil dan menjatuhkannya. Peringatan dari pendekatan ini adalah bahwa, selama kompilasi, dibutuhkan lebih banyak waktu karena menyentuh setiap pernyataan dan menyimpan bundel dalam memori selama proses. Itulah alasan utama mengapa kinerja bundling menjadi perhatian yang lebih besar bagi semua orang dan mengapa bahasa yang dikompilasi dimanfaatkan dalam alat untuk pengembangan web. Misalnya, esbuild adalah bundler yang ditulis dalam Go, dan SWC adalah kompiler TypeScript yang ditulis dalam Rust yang terintegrasi dengan Spark, bundler yang juga ditulis dalam Rust.

Untuk lebih memahami pengangkatan ruang lingkup, saya sangat merekomendasikan dokumentasi Parcel versi 2.

Hindari Transpiling Dini

Ada satu masalah khusus yang sayangnya agak umum dan dapat menghancurkan pohon-pohon gemetar. Singkatnya, ini terjadi ketika Anda bekerja dengan pemuat khusus, mengintegrasikan berbagai kompiler ke bundler Anda. Kombinasi umum adalah TypeScript, Babel, dan Webpack — dalam semua kemungkinan permutasi.

Baik Babel dan TypeScript memiliki kompilernya sendiri, dan pemuatnya masing-masing memungkinkan pengembang untuk menggunakannya, untuk integrasi yang mudah. Dan di situlah letak ancaman yang tersembunyi.

Kompiler ini mencapai kode Anda sebelum pengoptimalan kode. Dan apakah secara default atau salah konfigurasi, kompiler ini sering menampilkan modul CommonJS, bukan ESM. Seperti disebutkan di bagian sebelumnya, modul CommonJS bersifat dinamis dan, oleh karena itu, tidak dapat dievaluasi dengan benar untuk penghapusan kode mati.

Skenario ini menjadi lebih umum saat ini, dengan pertumbuhan aplikasi "isomorfik" (yaitu aplikasi yang menjalankan kode yang sama baik di sisi server maupun sisi klien). Karena Node.js belum memiliki dukungan standar untuk ESM, ketika kompiler ditargetkan ke lingkungan node , mereka mengeluarkan CommonJS.

Jadi, pastikan untuk memeriksa kode yang diterima algoritme pengoptimalan Anda .

Daftar Periksa Mengguncang Pohon

Sekarang setelah Anda mengetahui seluk beluk cara kerja bundling dan tree-shaking, mari buat daftar periksa yang dapat Anda cetak di suatu tempat yang berguna ketika Anda meninjau kembali implementasi dan basis kode Anda saat ini. Mudah-mudahan, ini akan menghemat waktu Anda dan memungkinkan Anda untuk mengoptimalkan tidak hanya kinerja yang dirasakan dari kode Anda, tetapi bahkan mungkin waktu pembuatan pipa Anda!

  1. Gunakan ESM, dan tidak hanya dalam basis kode Anda sendiri, tetapi juga pilih paket yang menampilkan ESM sebagai bahan habis pakainya.
  2. Pastikan Anda tahu persis mana (jika ada) dependensi Anda yang belum mendeklarasikan sideEffects atau menetapkannya sebagai true .
  3. Manfaatkan anotasi sebaris untuk mendeklarasikan pemanggilan metode yang murni saat menggunakan paket dengan efek samping.
  4. Jika Anda mengeluarkan modul CommonJS, pastikan untuk mengoptimalkan bundel Anda sebelum mengubah pernyataan impor dan ekspor.

Otorisasi Paket

Mudah-mudahan, pada titik ini kita semua setuju bahwa ESM adalah jalan ke depan dalam ekosistem JavaScript. Namun, seperti biasa dalam pengembangan perangkat lunak, transisi bisa jadi rumit. Untungnya, pembuat paket dapat mengadopsi langkah-langkah yang tidak melanggar untuk memfasilitasi migrasi yang cepat dan mulus bagi penggunanya.

Dengan beberapa tambahan kecil pada package.json , paket Anda akan dapat memberi tahu bundler lingkungan yang didukung oleh paket dan cara terbaik mendukungnya. Berikut daftar periksa dari Skypack:

  • Sertakan ekspor ESM.
  • Tambahkan "type": "module" .
  • Tunjukkan titik masuk melalui "module": "./path/entry.js" (konvensi komunitas).

Dan inilah contoh yang dihasilkan ketika semua praktik terbaik diikuti dan Anda ingin mendukung lingkungan web dan Node.js:

 { // ... "main": "./index-cjs.js", "module": "./index-esm.js", "exports": { "require": "./index-cjs.js", "import": "./index-esm.js" } // ... }

Selain itu, tim Skypack telah memperkenalkan skor kualitas paket sebagai tolok ukur untuk menentukan apakah paket tertentu disiapkan untuk umur panjang dan praktik terbaik. Alat ini bersumber terbuka di GitHub dan dapat ditambahkan sebagai devDependency ke paket Anda untuk melakukan pemeriksaan dengan mudah sebelum setiap rilis.

Membungkus

Saya harap artikel ini bermanfaat bagi Anda. Jika demikian, pertimbangkan untuk membagikannya dengan jaringan Anda. Saya berharap dapat berinteraksi dengan Anda di komentar atau di Twitter.

Sumber Daya yang Berguna

Artikel dan Dokumentasi

  • “Modul ES: Kartun Deep-Dive”, Lin Clark, Mozilla Hacks
  • "Gemetar Pohon", Webpack
  • "Konfigurasi", Paket Web
  • "Pengoptimalan", Paket Web
  • "Pengangkatan Lingkup", dokumentasi Parcel versi 2

Proyek dan Alat

  • Terser
  • babel-plugin-transform-import
  • paket skypack
  • paket web
  • Paket
  • Gulungan
  • membangun
  • SWC
  • Pengecekan Paket