Tree-Shaking: A Reference Guide

Sebelum memulai perjalanan kita untuk mempelajari apa itu getar pohon dan bagaimana mempersiapkan diri kita untuk sukses dengannya, kita perlu memahami modul apa saja yang ada dalam ekosistem JavaScript.

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

Untuk mendalami apa itu modul dan bagaimana cara kerjanya, saya merekomendasikan “ ES Modules: A Cartoon Deep-Dive ”. Tetapi untuk memahami nuansa gemetar pohon dan konsumsi modul, definisi di atas sudah cukup.

Apa Sebenarnya Arti Menggoyang Pohon?

Sederhananya, gemetar pohon berarti menghapus kode yang tidak dapat dijangkau (juga dikenal sebagai kode mati) dari sebuah bundel. Seperti yang dinyatakan dalam dokumentasi Webpack versi 3:

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

Istilah ini pertama kali dipopulerkan di komunitas front-end oleh tim Rollup . Tetapi penulis dari semua bahasa dinamis telah bergumul dengan masalah ini sejak jauh sebelumnya. Ide tentang algoritme yang mengguncang pohon dapat ditelusuri kembali setidaknya ke awal 1990-an.

Di lahan JavaScript, getaran pohon dapat terjadi sejak spesifikasi modul ECMAScript (ESM) di ES2015, 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 utamanya adalah karena ESM bersifat statis. Mari kita membedah apa artinya itu.

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 fungsi tersebut menambahkannya ke cakupan selama runtime.

require adalah function seperti yang lain dalam suatu program membuatnya cukup sulit untuk mengevaluasi hasil panggilannya pada waktu kompilasi. Di atas itu adalah fakta bahwa menambahkan 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 yang luas, spesifikasi ESM telah ditetapkan pada arsitektur baru ini, di mana modul diimpor dan diekspor dengan masing-masing kata kunci import dan export . Oleh karena itu, tidak ada lagi panggilan fungsional. ESM juga diizinkan hanya sebagai deklarasi tingkat atas – menyarangkannya di struktur lain tidak dimungkinkan, karena bersifat statis : ESM tidak bergantung pada eksekusi waktu proses.

Ruang Lingkup dan Efek Samping

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

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

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

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

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

Jadi, jika Anda adalah pengembang paket, gunakan sideEffects sebelum menerbitkan, dan, tentu saja, lakukan revisi pada setiap rilis untuk menghindari perubahan yang tidak terduga.

Selain sideEffects , juga dimungkinkan untuk menentukan kemurnian berdasarkan file-demi-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, dilakukan jika sebuah paket belum mendeklarasikan sideEffects: false atau jika perpustakaan memang menghadirkan efek samping pada metode tertentu.

Mengoptimalkan Webpack

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

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

File webpack.config.js memiliki properti root bernama mode . Kapan pun nilai properti ini adalah production , itu akan mengguncang 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:

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

Bukan kebetulan bahwa nilai pemicunya adalah production . Anda tidak ingin dependensi Anda dioptimalkan sepenuhnya dalam lingkungan pengembangan karena hal itu akan mempersulit proses debug. Jadi saya akan menyarankan untuk melakukannya dengan salah satu dari dua pendekatan.

Di satu sisi, Anda dapat memberikan 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 webpack.config.js :

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

Dalam kasus ini, Anda harus ingat untuk meneruskan --NODE_ENV=production di pipeline penerapan Anda.

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

Webpack Versi 3 ke Bawah

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

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

Seperti disebutkan di atas, kompilator tidak memiliki cara untuk mencari tahu sendiri saat sebuah paket merusak cakupan global. Tapi itu bukan satu-satunya situasi di mana ia melewatkan guncangan 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:

  • memanggil fungsi dari modul pihak ketiga yang tidak dapat diperiksa oleh compiler,
  • fungsi ekspor ulang yang diimpor dari modul pihak ketiga.

Sebuah alat yang dapat membantu kompilator untuk mengguncang pohon untuk bekerja adalah babel-plugin-transform-import . Ini akan membagi semua anggota dan ekspor bernama menjadi ekspor default, memungkinkan modul untuk dievaluasi satu per satu.

 // 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.

Scope Hoisting dan Compile Times

Di masa CommonJS, sebagian besar bundler hanya akan membungkus setiap modul dalam deklarasi fungsi lain dan memetakannya di dalam sebuah objek. Itu tidak ada bedanya dengan objek peta manapun 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 menggabungkan 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; ditambah, ini memungkinkan evaluasi kode untuk dengan mudah menemukan modul yang tidak dipanggil dan menjatuhkannya. Keberatan dari pendekatan ini adalah, selama kompilasi, dibutuhkan lebih banyak waktu karena menyentuh setiap pernyataan dan menyimpan bundel dalam memori selama proses. Itulah alasan besar 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 Transparansi Dini

Ada satu masalah khusus yang sayangnya cukup umum dan dapat menghancurkan pohon-pohon. Singkatnya, ini terjadi saat Anda bekerja dengan pemuat khusus, mengintegrasikan berbagai kompiler ke bundler Anda. Kombinasi yang umum adalah TypeScript, Babel, dan Webpack – dengan semua permutasi yang memungkinkan.

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

Kompiler ini mencapai kode Anda sebelum pengoptimalan kode. Dan apakah secara default atau salah konfigurasi, compiler ini sering mengeluarkan modul CommonJS, bukan ESM. Seperti yang 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 di sisi server dan sisi klien). Karena Node.js belum memiliki dukungan standar untuk ESM, ketika kompiler ditargetkan ke node , mereka mengeluarkan CommonJS.

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

Daftar Periksa Mengguncang Pohon

Sekarang setelah Anda mengetahui seluk beluk tentang cara kerja bundling dan penguncian pohon, mari kita menggambar daftar periksa yang dapat Anda cetak di suatu tempat yang berguna ketika Anda mengunjungi 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 mungkin bahkan waktu pembuatan pipeline Anda!

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

Pembuatan Paket

Mudah-mudahan, pada titik ini kita semua setuju bahwa ESM adalah jalan maju dalam ekosistem JavaScript. Seperti biasa dalam pengembangan perangkat lunak, transisi bisa jadi rumit. Untungnya, pembuat paket dapat mengadopsi langkah-langkah tanpa henti untuk memfasilitasi migrasi yang cepat dan mulus bagi pengguna mereka.

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

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

Dan berikut adalah 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

Semoga 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 Berguna

Artikel dan Dokumentasi

Proyek dan Alat

May 14, 2021

codeorayo

Ampuh! Ini rahasia mengembangkan aplikasi secara instan, tinggal download dan kembangkan. Gabung sekarang juga! Premium Membership [PRIVATE] https://premium.codeorayo.com

Leave a Reply

Your email address will not be published. Required fields are marked *