Tree Shaking React Native Apps

Nov 28 2022
Bagaimana kami menerapkan teknik pengoptimalan yang umum untuk aplikasi web ke aplikasi React Native kami untuk menurunkan waktu startup sebesar 20%. Pohon apa? Memang, Tree Shaking mungkin merupakan istilah yang membingungkan.

Bagaimana kami menerapkan teknik pengoptimalan yang umum untuk aplikasi web ke aplikasi React Native kami untuk menurunkan waktu startup sebesar 20%.

Pohon apa?

Memang, Tree Shaking mungkin merupakan istilah yang membingungkan. Anda mungkin pernah mendengarnya sebagai "impor elision" di TypeScript. Tree Shaking adalah bentuk penghapusan kode mati yang terkait secara khusus dengan penghapusan ekspor yang tidak digunakan. Jika kita akan menggabungkan semua modul, ekspor yang tidak digunakan secara efektif merupakan kode mati dan dapat dihapus. Namun, proses penentuan ekspor yang tidak terpakai bukanlah hal yang sepele. Tree Shaking biasanya diimplementasikan pada level compiler/bundler (misalnya Webpack atau ESBuild ) bukan oleh mesin JavaScript (seperti V8 atau Hermes). Banyak pola dalam JavaScript yang dapat merusak Tree Shaking tetapi dalam artikel ini saya ingin fokus pada satu aspek: sistem modul. Dua sistem modul yang relevan yang perlu kita pahami di sini adalah modul CommonJS danmodul ES .

CommonJS digunakan saat Anda menulis module.exports = {}atau exports.someMethod = () => {}. Modul ES diidentifikasi oleh importdan exportsintaks. Lebih sulit bagi kompiler untuk menerapkan Tree Shaking ke kode menggunakan CommonJS daripada modul ES. Modul CommonJS seringkali bersifat dinamis sementara modul ES dapat dianalisis secara statis. Misalnya, tidak mudah untuk mendeteksi semua pengidentifikasi ekspor dalam kode berikut secara statis:

Karena modul ES dapat dianalisis secara statis berdasarkan desain, kompiler lebih mudah mendeteksi ekspor yang tidak digunakan.
Oleh karena itu, demi kepentingan terbaik Anda, berikan modul ES kompiler pengoptimal untuk digunakan dan bukan modul CommonJS.

Latar belakang

Sebelum saya bergabung dengan Klarna, saya tidak memiliki pengalaman bekerja dengan React Native. Selama refactoring rutin saya menerapkan diff berikut:

Dengan asumsi bahwa bundler yang digunakan akan dianggap someFeatureMethodtidak terpakai jika SOME_STATIC_FLAGsalah dan dengan demikian dihapus some-feature-moduledari bundel terakhir. Selama peninjauan kode, perbedaan ini ditandai sebagai bermasalah, jadi saya duduk dan memeriksa ulang asumsi saya dan di mana asumsi tersebut rusak. Untungnya kami sudah beralih ke Webpack (dalam bentuk Re.Pack ) beberapa bulan sebelumnya untuk mengaktifkan pemisahan bundel denganReact.lazy . Hal ini mempermudah saya untuk mengonfigurasi proses bundling sedemikian rupa sehingga saya dapat memeriksa bundel JavaScript terakhir. Hanya Hermes yang perlu dinonaktifkan dalam kasus kami untuk melihat hasil akhir JavaScript.

Setelah beberapa percobaan dan kesalahan untuk membuatnya lebih mudah menemukan di mana some-feature-modulediimpor, saya melihat baris berikut: c=(n(463526),n(456189)Operator koma adalah sesuatu yang biasanya tidak Anda gunakan, jadi izinkan saya meringkas apa yang dilakukannya: Ini mengevaluasi semua operan dan hanya menggunakan nilai pengembalian dari operan terakhir. Dengan kata lain, nilai pengembalian dari n(463526)tidak digunakan. Karena saya sudah memiliki pengalaman bekerja dengan tree-shaking di web, cukup jelas bagi saya apa ini sebelum minifikasi: require('some-feature-module')(Webpack mengubah string sumber impor menjadi angka).

Webpack sebenarnya mengenali yang someFeatureMethodtidak digunakan dan dengan demikian menghapus penggunaannya. Namun, Webpack menebus penghapusan ekspor yang tidak digunakan dari modul dan dengan demikian menyimpan impor karena tidak tahu apakah modul tersebut memiliki efek samping. Jika sebuah modul memiliki efek samping, kita tidak bisa menghapusnya begitu saja dari bundel karena itu akan mengubah aliran program.

Yang harus kami lakukan untuk membuat diff asli berfungsi seperti yang diharapkan, adalah memastikan Tree Shaking diterapkan ke bundel terakhir.

Penerapan

Semuanya bermuara pada memastikan bahwa Anda tidak mengubah modul ES ke CommonJS sebelum Webpack menggabungkan semua modul. Jika Anda menggunakan preset Metro Babel (default untuk aplikasi React Native yang baru), sebagian besar pekerjaan dilakukan untuk mengaktifkan disableImportExportTransform:

Perhatikan bahwa opsi ini saat ini tidak didokumentasikan dan dapat dihapus kapan saja.

Kami juga perlu memberi tahu Webpack untuk menggunakan titik masuk yang menggunakan modul ES, bukan modul CommonJS. Untuk file individu yang berarti memilih .mjssementara untuk paket kami perlu memberi tahu Webpack untuk menggunakan modulebidang utama .

Namun, ini mengungkapkan masalah dengan cara kami menulis JavaScript dan cara kode ditulis dalam ekosistem React Native. Kami telah mengidentifikasi 3 kelas masalah.

Mengekspor berbagai sintaks di maindanmodule

Bidang utama ini hanya boleh digunakan untuk membedakan sistem modul ( mainuntuk CommonJS, moduleuntuk modul ES). Namun, banyak paket mengirimkan sintaks yang lebih modern dari titik masuk module. Misalnya, classsintaks saat ini tidak didukung oleh Hermes .

Untuk saat ini, kami mengubah semua node_moduleskonten menjadi sintaks ES5 atau lebih tepatnya sintaks yang didukung oleh Hermes dengan menambahkan kustom ruleke konfigurasi Webpack:

Mengimpor modul CommonJS dengan sintaks yang ambigu

Webpack tidak akan dapat menemukan ekspor dari modul yang menggabungkan sistem modul. Namun, React Native sendiri mengirimkan file sumbernya dengan sistem modul campuran misalnya

Solusinya di sini adalah terus mentranspilasi modul-modul ini ke CommonJS (sehingga menonaktifkan Tree Shaking) dengan menambahkan khusus ruleke konfigurasi Webpack:

Pengidentifikasi impor tidak ada

Ini sebenarnya adalah SyntaxError dalam JavaScript yang sebagian besar tidak disadari. Misalnya, import { doesNotExist } from 'some-module';akan melempar file SyntaxError. Ini sebagian besar merupakan gangguan bagi pengembang tetapi dapat menyebabkan masalah runtime yang sebenarnya. Kami menerapkan penerapan modul ES yang ketat ini di Webpack dengan mengaktifkan module.parser.javascript.exportsPresencekonfigurasi Webpack.

Sebagian besar masalah ini disebabkan oleh pengeksporan ulang tipe dalam TypeScript misalnya

Untungnya, TypeScript dapat menandai masalah ini pada level tipe dengan mengaktifkan isolatedModules:

typepengubah pada nama impor baru di TypeScript 4.5. Menambahkan dukungan untuk typepengubah pada nama impor cukup menantang karena kami perlu memutakhirkan parser ESLint yang kami gunakan , Prettier dan TypeScript.

Menambahkan typepengubah untuk mengimpor nama menghasilkan Babel menghapus impor jenis yang sebenarnya tidak ada saat runtime.

Hasil

Implementasi awal cukup hacky. Namun, hasil pertama sudah menunjukkan peningkatan startup rata-rata 20% di kedua platform (Samsung Galaxy S9 2.2s turun dari 2.8s dan iPhone 11 640ms turun dari 802ms).

Apa yang kami lihat adalah pengurangan 46% dari potongan JavaScript awal kami yang kritis. Ukuran keseluruhan JavaScript yang kami kirim turun sebesar 14%. Perbedaannya sebagian besar disebabkan oleh pemindahan kode dari potongan utama ke potongan async (fitur dan rute).

Perbedaan absolut dari statistik bundel setelah Tree Shaking
Perbedaan relatif statistik bundel setelah Tree Shaking

Gambar-gambar ini dibuat oleh statoscope.tech yang luar biasa yang membantu kami menganalisis perubahan ini dan akan terus membantu kami mendorong peningkatan lebih lanjut pada ukuran bundel.

Perhatikan bahwa pengurangan tidak hanya berasal dari penghapusan ekspor yang tidak digunakan tetapi juga ModuleConcatenationPluginkemampuan Webpack untuk menggabungkan lebih banyak modul. Dengan kata lain, kita dapat menjangkau lebih banyak modul . Kami belum sepenuhnya memanfaatkan pengangkat lingkup. Saat ini hanya 20% dari modul yang diangkat. Kami mengharapkan lebih banyak ukuran bundel dan keuntungan runtime setelah kami meningkatkan jumlah itu.

Pengurangan 40% dalam peta JavaScript hampir persis 1:1 dengan waktu yang diperlukan untuk mengevaluasi bundel JavaScript sebelum dapat dieksekusi. Mengevaluasi JavaScript memblokir waktu startup yang dihasilkan sehingga mengurangi jumlah JavaScript yang dikirimkan secara langsung akan mengurangi waktu startup.

Setelah langkah terakhir implementasi dilakukan 2 minggu kemudian, kami masih mendapatkan hasil yang sama di lab kami dan siap untuk mendaratkan fitur ini di cabang utama kami. Kami sangat berhati-hati untuk melakukan perubahan terakhir langsung setelah batas waktu rilis kami. Ini memungkinkan kami menguji sistem modul baru secara ekstensif dalam versi aplikasi internal yang digunakan oleh karyawan kami. Setelah satu minggu pengujian internal, fitur tersebut diluncurkan secara bertahap kepada pengguna akhir kami. Kami sangat berharap setelah kami melihat bahwa stabilitas aplikasi sebagian besar tidak terpengaruh. Data produksi menunjukkan peningkatan relatif yang sama pada waktu mulai rata-rata yang kami lihat di hasil lab kami:

Versi 22.37 tanpa guncangan pohon; 22.38 dengan goncangan pohon

Data produksi untuk pengguna Android
Data produksi untuk pengguna iOS

Peningkatan ini datang dengan mengorbankan waktu pembangunan yang lebih lama. Membundel bundel JavaScript produksi membutuhkan waktu sekitar 30% (4 menit) lebih lama. Kami dengan senang hati mengambil peningkatan waktu pembuatan ini karena secara langsung diterjemahkan menjadi pengalaman pengguna yang lebih baik. Beberapa peningkatan waktu build dikaitkan dengan transpilasi lebih dari yang kami butuhkan. Implementasi awal tidak menghabiskan waktu untuk mengurangi jumlah yang perlu kita ubah. Kami juga akan mengganti beberapa peningkatan waktu pembuatan, semakin banyak paket yang mengirimkan modul ES yang tepat. Perlu diingat bahwa waktu bundling JavaScript bukan satu-satunya tugas yang diperlukan untuk membangun aplikasi React Native. Dengan mengkompilasi binari, dll, peningkatan bundel JavaScript pada akhirnya tidak terlalu berdampak.

Apa berikutnya

Sepertinya modul ES belum dikerjakan secara aktif di ekosistem React Native. Kami ingin lebih menyelaraskan ekosistem pada penggunaan modul ES yang tepat (mis module. entri yang menunjuk ke JavaScript dengan sintaks yang setara). Dengan begitu kita dapat mengurangi konfigurasi build dan transpile lebih sedikit.

Meskipun ada dukungan untuk menggunakan modul ES di belakang bendera di Metro ( experimentalImportSupport), itu ditandai sebagai percobaan dan tidak didokumentasikan. Mengaktifkan flag tersebut dalam pengembangan tidak bekerja untuk kami (belum) tetapi kami berharap suatu hari nanti kami dapat menggunakan sistem modul yang sama dalam pengembangan dan produksi. Kami ingin memulai kembali diskusi tentang modul ES di React native karena sepertinya dukungan untuk modul ES saat ini tidak sedang dikerjakan secara aktif. Dukungan Tree Shaking bahkan benar-benar ditinggalkan bertahun-tahun yang lalu. .

Lagi pula, modul ES adalah fitur bahasa yang pada akhirnya dipelajari oleh semua orang yang mengetahui JavaScript. Kami tidak melihat alasan mengapa React Native harus memiliki langkah pembelajaran tambahan untuk memahami pemecahan bundel dan penghapusan kode mati.

Tulis sekali, jalankan di mana saja!

Apakah Anda menikmati postingan ini? Ikuti Klarna Engineering di Medium dan LinkedIn untuk menantikan lebih banyak artikel seperti ini.