Saat aplikasi Anda berkembang dan tumbuh, waktu pembuatannya juga meningkat - dari beberapa menit selama perakitan ulang dalam mode pengembangan hingga puluhan menit selama perakitan produksi "dingin". Ini benar-benar tidak dapat diterima. Kami, pengembang, tidak suka untuk beralih konteks sambil menunggu bundel siap dan ingin menerima umpan balik dari aplikasi sedini mungkin - idealnya sambil beralih dari IDE ke browser.
Bagaimana cara mencapai ini? Apa yang dapat kita lakukan untuk mengoptimalkan waktu pembuatan?
Artikel ini adalah ikhtisar alat yang ada di ekosistem webpack untuk mempercepat perakitan, pengalaman dan tip mereka.
Optimalisasi ukuran bundel dan kinerja aplikasi itu sendiri tidak dipertimbangkan dalam artikel ini.
Proyek, yang direferensikan dalam teks dan relatif yang pengukuran kecepatan perakitan dilakukan, adalah aplikasi yang relatif kecil yang ditulis pada JS + Flow + React + Redux stack menggunakan webpack, Babel, PostCSS, Sass, dll dan terdiri dari sekitar 30 ribu baris kode dan 1.500 modul. Versi ketergantungan saat ini pada April 2019.
Studi dilakukan pada komputer dengan Windows 10, Node.js 8, prosesor 4-core, 8 GB memori dan SSD.
Terminologi
- Majelis adalah proses mengubah file sumber proyek ke satu set aset terkait yang bersama-sama membuat aplikasi web.
- mode-dev - perakitan dengan
mode: 'development'
opsi mode: 'development'
, biasanya menggunakan webpack-dev-server dan mode menonton. - prod-mode - perakitan dengan
mode: 'production'
opsi mode: 'production'
, biasanya dengan set lengkap optimasi bundel. - Bangun tambahan - dalam mode dev: membangun kembali hanya file dengan perubahan.
- "Cold" build - build from scratch, tanpa cache, tetapi dengan dependensi yang diinstal.
Caching
Caching memungkinkan Anda untuk menyimpan hasil perhitungan untuk digunakan kembali lebih lanjut. Rakitan pertama mungkin sedikit lebih lambat dari biasanya karena overhead caching, tetapi yang berikutnya akan jauh lebih cepat karena penggunaan kembali hasil kompilasi modul yang tidak berubah.
Secara default, webpack dalam mode arloji menyimpan hasil build menengah di dalam memori agar tidak memasang kembali seluruh proyek dengan setiap perubahan. Untuk bangunan normal (bukan dalam mode tontonan), pengaturan ini tidak masuk akal. Anda juga dapat mencoba mengaktifkan penyelesaian cache untuk menyederhanakan pencarian webpack untuk modul dan melihat apakah pengaturan ini memiliki efek nyata pada proyek Anda.
Tidak ada cache yang persisten (disimpan ke disk atau penyimpanan lain) di webpack, meskipun mereka berjanji untuk menambahkannya dalam versi 5. Sementara itu, kita dapat menggunakan alat berikut:
- Caching dalam pengaturan TerserWebpackPlugin
Dinonaktifkan secara default. Bahkan sendiri itu memiliki efek positif yang nyata: 60,7 s β 39 s (-36%), cocok dengan alat caching lainnya.
Mengaktifkan dan menggunakan sangat sederhana:
optimization: { minimizer: [ new TerserJsPlugin({ terserOptions: { ... }, cache: true }) ] }
- cache-loader
Cache-loader dapat ditempatkan di rantai loader apa saja dan menyimpan cache hasil dari loader sebelumnya.
Secara default, ini menyimpan cache ke folder .cache-loader di root proyek. Menggunakan opsi cacheDirectory
dalam pengaturan loader, path dapat didefinisikan ulang.
Contoh penggunaan:
{ test: /\.js$/, use: [ { loader: 'cache-loader', options: { cacheDirectory: path.resolve( __dirname, 'node_modules/.cache/cache-loader' ), }, }, 'babel-loader' ] }
Solusi aman dan andal. Ia bekerja tanpa masalah dengan hampir semua loader: untuk skrip (babel-loader, ts-loader), gaya (scss-, less-, postcss-, css-loader), gambar dan font (image-webpack-loader, react-svg- loader, pemuat file), dll.
Harap dicatat:
- Saat menggunakan cache-loader bersamaan dengan style-loader atau MiniCssExtractPlugin.loader, itu harus ditempatkan setelahnya :
['style-loader', 'cache-loader', 'css-loader', ...]
. - Berlawanan dengan rekomendasi dokumentasi untuk menggunakan loader ini untuk menyimpan hasil dari perhitungan yang melelahkan saja, itu mungkin memberikan peningkatan kinerja yang kecil namun terukur untuk loader "yang lebih ringan" - Anda perlu mencoba dan mengukur.
Hasil:
- dev: 35.5 s β (aktifkan cache-loader) β 36.2 s (+ 2%) β (reassembly) β 7.9 s (-78%)
- prod: 60,6 detik β (aktifkan cache-loader) β 61,5 detik (+ 1,5%) β (pemasangan kembali) β 30,6 detik (-49%) β (aktifkan cache untuk Terser) β 15, 4 s (-75%)
- HardSourceWebpackPlugin
Solusi yang lebih besar dan "pintar" untuk caching di tingkat seluruh proses perakitan, daripada rantai loader individu. Dalam kasus penggunaan dasar, cukup menambahkan plugin ke konfigurasi webpack, pengaturan standar harus cukup untuk operasi yang benar. Cocok untuk mereka yang ingin mencapai kinerja maksimal dan tidak takut menghadapi kesulitan.
plugins: [ ..., new HardSourceWebpackPlugin() ]
Dokumentasi berisi contoh-contoh penggunaan dengan pengaturan lanjutan dan tips untuk memecahkan masalah yang mungkin terjadi. Sebelum menempatkan plug-in ke dalam operasi secara berkelanjutan, ada baiknya menguji secara menyeluruh operasinya dalam berbagai situasi dan mode perakitan.
Hasil:
- dev: 35.5 s β (aktifkan plugin) β 36.5 s (+ 3%) β (reassembly) β 3.7 s (-90%)
- prod: 60,6 s β (aktifkan plugin) β 69.5 s (+ 15%) β (reassembly) β 25 s (-59%) β (nyalakan cache untuk Terser) β 10 s (-83%)
Pro:
- Dibandingkan dengan cache-loader, ini mempercepat perakitan lebih banyak lagi;
- Itu tidak memerlukan deklarasi duplikat di berbagai tempat konfigurasi, seperti di cache-loader.
Cons:
- Dibandingkan dengan cache-loader, ini memperlambat build pertama (ketika tidak ada cache disk);
- dapat sedikit meningkatkan waktu bangun kembali secara bertahap;
- dapat menyebabkan masalah saat menggunakan webpack-dev-server dan memerlukan konfigurasi terperinci pemisahan dan pembatalan cache (lihat dokumentasi );
- beberapa masalah dengan bug di GitHub.
- Caching dalam pengaturan babel-loader . Dinonaktifkan secara default. Efeknya beberapa persen lebih buruk daripada dari cache-loader.
- Caching dalam pengaturan eslint-loader . Dinonaktifkan secara default. Jika Anda menggunakan pemuat ini, cache akan membantu Anda untuk tidak membuang waktu melapisi file yang tidak berubah selama pemasangan kembali.
Saat menggunakan cache-loader atau HardSourceWebpackPlugin, Anda perlu menonaktifkan mekanisme caching bawaan di plugins atau loader lainnya (kecuali untuk TerserWebpackPlugin), karena mereka akan berhenti berguna dalam membangun berulang dan bertahap, dan yang "dingin" bahkan akan melambat. Hal yang sama berlaku untuk cache-loader itu sendiri jika HardSourceWebpackPlugin sudah digunakan.
Saat mengatur caching, pertanyaan berikut mungkin muncul:
Di mana hasil caching harus disimpan?
node_modules/.cache/<_>/
biasanya disimpan di node_modules/.cache/<_>/
. Sebagian besar alat menggunakan jalur ini secara default dan memungkinkan Anda untuk menimpanya jika Anda ingin menyimpan cache di tempat lain.
Kapan dan bagaimana cara membatalkan cache?
Sangat penting untuk membersihkan cache ketika perubahan dilakukan pada konfigurasi rakitan, yang akan memengaruhi output. Menggunakan cache lama dalam kasus seperti itu berbahaya dan dapat menyebabkan kesalahan yang tidak diketahui.
Faktor yang perlu dipertimbangkan:
- daftar dependensi dan versinya: package.json, package-lock.json, yarn.lock, .yarn-integrity;
- konten webpack, Babel, PostCSS, daftar browser, dan file konfigurasi lainnya yang secara eksplisit atau implisit digunakan oleh loader dan plugin.
Jika Anda tidak menggunakan cache-loader atau HardSourceWebpackPlugin, yang memungkinkan Anda untuk mendefinisikan kembali daftar sumber untuk membentuk sidik jari rakitan, skrip npm yang menghapus cache saat menambahkan, memperbarui, atau menghapus dependensi akan membantu Anda sedikit lebih mudah:
"prunecaches": "rimraf ./node_modules/.cache/", "postinstall": "npm run prunecaches", "postuninstall": "npm run prunecaches"
Nodemon yang dikonfigurasikan untuk menghapus cache dan memulai kembali webpack-dev-server ketika mendeteksi perubahan pada file konfigurasi juga akan membantu:
"start": "cross-env NODE_ENV=development nodemon --exec \"webpack-dev-server --config webpack.config.dev.js\""
nodemon.json
{ "watch": [ "webpack.config.dev.js", "babel.config.js", "more configs...", ], "events": { "restart": "yarn prunecaches" } }
Apakah saya perlu menyimpan cache di repositori proyek?
Karena cache sebenarnya adalah artefak assembly, maka tidak perlu untuk mengkomitnya ke repositori. Lokasi cache di dalam folder node_modules, yang, sebagai suatu peraturan, termasuk dalam .gitignore, akan membantu dengan ini.
Perlu dicatat bahwa jika ada sistem caching yang andal dapat menentukan validitas cache dalam kondisi apa pun, termasuk mengubah OS dan versi Node.js, cache dapat digunakan kembali di antara mesin pengembangan atau dalam CI, yang akan secara drastis mengurangi waktu bahkan saat membangun pertama setelah beralih antar cabang.
Dalam mode build apa itu layak dan di mana itu tidak layak menggunakan cache?
Tidak ada jawaban yang pasti di sini: semuanya tergantung pada seberapa intensif Anda menggunakan mode dev dan prod selama pengembangan dan beralih di antara mereka. Secara umum, tidak ada yang mencegah menyalakan caching di mana-mana, tetapi ingat bahwa biasanya memperlambat pembangunan pertama. Di CI, Anda mungkin selalu membutuhkan bangunan "bersih", di mana caching dapat dinonaktifkan menggunakan variabel lingkungan yang sesuai.
Materi menarik tentang caching di webpack:
Paralelisasi
Dengan menggunakan paralelisasi, Anda bisa mendapatkan peningkatan kinerja dengan menggunakan semua inti prosesor yang tersedia. Efek akhir adalah individual untuk setiap mobil.
Ngomong-ngomong, berikut adalah kode Node.js sederhana untuk mendapatkan jumlah core prosesor yang tersedia (mungkin berguna ketika menyiapkan alat yang tercantum di bawah):
const os = require('os'); const cores = os.cpus().length;
- Paralisasi dalam pengaturan TerserWebpackPlugin
Dinonaktifkan secara default. Seperti halnya cachingnya sendiri, caching mudah dinyalakan dan terasa mempercepat perakitan.
optimization: { minimizer: [ new TerserJsPlugin({ terserOptions: { ... }, parallel: true }) ] }
- pemuat benang
Thread-loader dapat ditempatkan dalam rantai loader yang melakukan perhitungan berat, setelah itu loader sebelumnya akan menggunakan kumpulan proses subproses Node.js (prosesor).
Ini memiliki satu set opsi yang memungkinkan Anda untuk menyempurnakan pekerjaan kumpulan pekerja, meskipun nilai-nilai dasar terlihat cukup memadai. poolTimeout
dan workers
berhak mendapatkan perhatian khusus - lihat contoh .
Ini dapat digunakan bersama dengan cache-loader sebagai berikut (urutan penting): ['cache-loader', 'thread-loader', 'babel-loader']
. Jika pemanasan diaktifkan untuk thread-loader, Anda harus memeriksa ulang stabilitas rakitan berulang yang menggunakan cache - webpack mungkin hang dan tidak menyelesaikan proses setelah perakitan berhasil diselesaikan. Dalam hal ini, matikan saja pemanasan.
Jika Anda menemukan hang build setelah menambahkan thread-loader ke rantai kompilasi bergaya Sass, tip ini mungkin membantu.
- happypack
Plugin yang memotong panggilan loader dan mendistribusikan pekerjaan mereka di beberapa utas. Saat ini, ia dalam mode dukungan (yaitu, pengembangan tidak direncanakan), dan pembuatnya merekomendasikan thread-loader sebagai pengganti. Jadi, jika proyek Anda selalu terbarui, lebih baik jangan menggunakan HappyPack, walaupun tentu saja patut dicoba dan membandingkan hasilnya dengan thread-loader.
HappyPack memiliki dokumentasi konfigurasi yang dapat dimengerti, yang, secara kebetulan, agak tidak biasa dalam dirinya sendiri: diusulkan untuk memindahkan konfigurasi loader ke panggilan konstruktor plug-in, dan mengganti rantai loader sendiri dengan loader happypack mereka sendiri. Pendekatan non-standar semacam itu dapat menyebabkan ketidaknyamanan saat membuat konfigurasi webpack khusus βdari bagianβ.
HappyPack mendukung daftar loader yang terbatas ; utama dan paling banyak digunakan dalam daftar ini ada, tetapi kinerja orang lain tidak dijamin karena kemungkinan ketidakcocokan API. Informasi lebih lanjut dapat ditemukan dalam masalah - masalah proyek.
Penolakan perhitungan
Pekerjaan apa pun membutuhkan waktu. Untuk menghabiskan lebih sedikit waktu, Anda harus menghindari pekerjaan yang tidak banyak berguna, dapat ditunda sampai nanti, atau tidak diperlukan sama sekali dalam situasi ini.
- Terapkan loader ke modul sesedikit mungkin
Pengujian, mengecualikan, dan menyertakan properti menentukan kondisi untuk menyertakan modul dalam proses pemrosesan oleh loader. Intinya adalah untuk menghindari transformasi modul yang tidak memerlukan transformasi ini.
Contoh yang populer adalah pengecualian node_modules dari transpilasi via Babel:
rules: [ { test: /\.jsx?$/, exclude: /node_modules/, loader: 'babel-loader' } ]
Contoh lain adalah bahwa file CSS biasa tidak perlu diproses oleh preprosesor:
rules: [ { test: /\.scss$/, use: ['style-loader', 'css-loader', 'sass-loader'] }, { test: /\.css$/, use: ['style-loader', 'css-loader'] } ]
- Jangan aktifkan optimasi ukuran bundel dalam mode dev
Pada mesin pengembang yang kuat dengan Internet stabil, aplikasi yang dikerahkan secara lokal biasanya dimulai dengan cepat, bahkan jika beratnya beberapa megabyte. Mengoptimalkan bundel selama perakitan dapat memakan waktu lebih berharga daripada menghemat beban.
Saran tersebut menyangkut JS (Terser, Uglify , dll. ), CSS (cssnano, optimalkan-css-assets-webpack-plugin), SVG dan gambar (SVGO, Imagemin, image-webpack-loader), HTML (html-minifier, opsi dalam html-webpack-plugin), dll.
- Jangan memasukkan polyfill dan transformasi dalam mode dev
Jika Anda menggunakan babel-preset-env, postcss-preset-env atau Autoprefixer - tambahkan konfigurasi Browserslist terpisah untuk mode-dev, yang hanya menyertakan browser yang Anda gunakan selama pengembangan. Kemungkinan besar, ini adalah versi terbaru dari Chrome atau Firefox yang secara sempurna mendukung standar modern tanpa polyfill dan transformasi. Ini akan menghindari pekerjaan yang tidak perlu.
Contoh .browserslistrc:
[production] your supported browsers go here... [development] last 2 Chrome versions last 2 Firefox versions last 1 Safari version
- Tinjau penggunaan peta sumber
Membuat peta sumber yang paling akurat dan lengkap membutuhkan waktu yang cukup lama (pada proyek kami - sekitar 30% dari waktu pembuatan dengan devtool: 'source-map'
). Pikirkan tentang apakah Anda memerlukan peta sumber dalam rakitan produk (secara lokal dan dalam CI). Mungkin layak untuk membuatnya hanya jika diperlukan - misalnya, berdasarkan pada variabel lingkungan atau tag pada komit.
Dalam mode dev, dalam kebanyakan kasus akan ada opsi yang agak ringan - 'cheap-eval-source-map'
atau 'cheap-module-eval-source-map'
. Lihat dokumentasi webpack untuk lebih jelasnya.
- Mengatur kompresi di Terser
Menurut dokumentasi Terser (hal yang sama berlaku untuk Uglify), saat mengecilkan kode, sebagian besar waktu dikonsumsi oleh opsi mangle
dan compress
. Dengan menyelaraskannya, Anda dapat mencapai akselerasi perakitan dengan biaya sedikit peningkatan ukuran bundel. Ada contoh dalam sumber vue-cli dan contoh lain dari seorang insinyur dari Slack. Dalam proyek kami, penyetelan Terser dalam perwujudan pertama mengurangi waktu rakitan sekitar 7% dengan imbalan 2,5 persen peningkatan ukuran bundel. Apakah game ini bernilai lilin itu terserah Anda.
- Kecualikan dependensi eksternal dari penguraian
Menggunakan opsi module.noParse
dan resolve.alias
Anda dapat mengarahkan kembali impor modul perpustakaan ke versi yang sudah dikompilasi dan cukup memasukkannya ke dalam bundel tanpa membuang waktu penguraian. Dalam mode dev, ini harus secara signifikan meningkatkan kecepatan perakitan, termasuk tambahan.
Algoritma ini kira-kira sebagai berikut:
(1) Buat daftar modul yang perlu dilewati saat parsing.
Idealnya, ini semua dependensi runtime yang termasuk dalam bundel (atau setidaknya yang paling masif dari mereka, seperti react-dom atau lodash), dan tidak hanya mereka sendiri (level pertama), tetapi juga transitif (dependensi dependensi). Di masa depan, Anda harus mempertahankan daftar ini sendiri.
(2) Untuk modul yang dipilih, tulis path ke versi yang dikompilasi.
Alih-alih melewatkan dependensi, Anda perlu memberikan kolektor dengan alternatif, dan alternatif ini tidak boleh bergantung pada lingkungan - memiliki panggilan ke module.exports
, require
, process
, import
, dll. Modul file tunggal yang sudah dikompilasi (belum tentu diperkecil), yang biasanya terletak di folder dist di dalam sumber dependensi, cocok untuk peran ini. Untuk menemukannya, Anda harus pergi ke node_modules. Misalnya, untuk axios, lintasan ke modul yang dikompilasi terlihat seperti ini: node_modules/axios/dist/axios.js
.
(3) Dalam konfigurasi webpack, gunakan opsi resol.alias untuk mengganti impor dengan nama dependensi dengan impor langsung file yang jalurnya ditulis pada langkah sebelumnya.
Sebagai contoh:
{ resolve: { alias: { axios: path.resolve( __dirname, 'node_modules/dist/axios.min.js' ), ... } } }
Ada kelemahan besar di sini: jika kode Anda atau kode dependensi Anda tidak merujuk ke titik entri standar (file indeks, bidang main
dalam package.json
), tetapi ke file tertentu di dalam sumber dependensi, atau jika dependensi diekspor sebagai modul-ES, atau jika proses penyelesaian mengganggu sesuatu (misalnya, babel-plugin-transform-import), seluruh ide mungkin gagal. Bundel akan berkumpul, tetapi aplikasi akan rusak.
(4) Dalam konfigurasi webpack, gunakan opsi module.noParse untuk melewati parsing modul yang telah dikompilasi yang diminta oleh lintasan dari langkah 2 menggunakan ekspresi reguler.
Sebagai contoh:
{ module: { noParse: [ new RegExp('node_modules/dist/axios.min.js'), ... ] } }
Intinya: di atas kertas, metode ini terlihat menjanjikan, tetapi pengaturan non-sepele dengan perangkap setidaknya meningkatkan biaya implementasi, dan paling tidak mengurangi manfaatnya.
Alternatif dengan prinsip operasi yang serupa adalah dengan menggunakan opsi externals
. Dalam hal ini, Anda harus secara mandiri menyisipkan tautan ke skrip eksternal dalam file HTML, dan bahkan dengan versi dependensi yang diperlukan yang sesuai dengan package.json.
- Pisahkan kode yang jarang berubah menjadi bundel yang terpisah dan kompilasi hanya sekali
Tentunya Anda pernah mendengar tentang DllPlugin . Dengan itu, Anda dapat mendistribusikan kode yang berubah secara aktif (aplikasi Anda) dan jarang mengubah kode (misalnya, dependensi) ke majelis yang berbeda. Setelah bundel dependensi yang dirakit (DLL yang sama) kemudian cukup dihubungkan ke perakitan aplikasi, ia menghemat waktu.
Ini terlihat seperti ini secara umum:
- Untuk membangun DLL, konfigurasi webpack terpisah dibuat, modul yang diperlukan terhubung sebagai titik masuk.
- Build dimulai dengan konfigurasi ini. DllPlugin menghasilkan bundel DLL dan file manifes dengan nama peta dan jalur modul.
- DllReferencePlugin ditambahkan ke konfigurasi rakitan utama, di mana manifes diteruskan.
- Impor dependensi yang diberikan dalam DLL selama perakitan dipetakan ke modul yang sudah dikompilasi menggunakan manifes.
Anda dapat membaca sedikit lebih banyak di artikel di sini .
Mulai menggunakan pendekatan ini, Anda akan segera menemukan sejumlah kelemahan:
- Rakitan DLL terisolasi dari rakitan utama, dan perlu dikelola secara terpisah: menyiapkan konfigurasi khusus, restart setiap kali cabang diaktifkan atau ketergantungan berubah.
- Karena DLL tidak terkait dengan artefak rakitan utama, itu perlu disalin secara manual ke folder dengan aset lain dan dimasukkan dalam file HTML menggunakan salah satu plugin ini: 1 , 2 .
- Adalah perlu untuk secara manual tetap up to date daftar dependensi yang dimaksudkan untuk dimasukkan dalam bundel DLL.
- Hal yang paling menyedihkan: pengocokan pohon tidak diterapkan pada bundel DLL. Secara teori, opsi
entryOnly
dimaksudkan untuk ini, tetapi mereka lupa mendokumentasikannya.
Anda dapat menyingkirkan boilerplate dan menyelesaikan masalah pertama (serta yang kedua, jika Anda menggunakan html-webpack-plugin v3 - tidak bekerja dengan versi 4) menggunakan AutoDllPlugin . Namun, itu masih tidak mendukung opsi entryOnly
untuk entryOnly
digunakan "di bawah tenda", dan penulis plugin meragukan kelayakan menggunakan gagasannya dalam terang webpack 5 mendatang.
Lain-lain
Perbarui perangkat lunak dan dependensi Anda secara teratur. Node.js, npm / yarn (webpack, Babel .) . , changelog, issues, , .
PostCSS postcss-preset-env stage, . , stage-3, Custom Properties, stage-4 13%.
Sass (node-sass, sass-loader), Dart Sass ( Sass Dart, JS) fast-sass-loader . , . β dart-sass , node-sass, JS, libsass.
Dart Sass sass-loader . Sass fibers.
CSS-, dev-. - , , , .
Contoh:
{ loader: 'css-loader', options: { modules: true, localIdentName: isDev ? '[path][name][local]' : '[hash:base64:5]' } }
, , : .
, - webpack PrefetchPlugin , , β . webpack issues , . ?
- . CLI-
--json
, . . , , dev- . - - Hints.
- , βLong module build chainsβ. , β PrefetchPlugin .
- PrefetchPlugin. . StackOverflow .
: .
, (TypeScript, Angular .) β !
Sumber
, , , .