Cara webpack membantu penyimpanan aset dalam cache
Hal berikutnya (setelah mengoptimalkan ukuran aplikasi yang meningkatkan waktu pemuatan aplikasi adalah penyimpanan dalam cache. Gunakan untuk mempertahankan bagian aplikasi di klien dan menghindari mendownload ulang kapan saja.
Menggunakan pembuatan versi paket dan header cache
Pendekatan umum dalam melakukan caching adalah dengan:
memberi tahu browser untuk menyimpan file dalam cache dalam waktu yang sangat lama (misalnya, satu tahun):
# Server header Cache-Control: max-age=31536000
Jika Anda belum mengetahui fungsi
Cache-Control
, lihat postingan Jake Archibald yang sangat bagus tentang praktik terbaik caching.dan ganti nama file saat diubah untuk memaksa download ulang:
<!-- Before the change --> <script src="./index-v15.js"></script> <!-- After the change --> <script src="./index-v16.js"></script>
Pendekatan ini akan memberi tahu browser untuk mendownload file JS, meng-cache-nya, dan menggunakan salinan yang di-cache. Browser hanya akan terhubung ke jaringan jika nama file berubah (atau jika setahun telah berlalu).
Dengan webpack, Anda melakukan hal yang sama, tetapi yang menentukan hash file, bukan nomor versi. Untuk menyertakan hash ke dalam nama file, gunakan
[chunkhash]
:
// webpack.config.js
module.exports = {
entry: './index.js',
output: {
filename: 'bundle.[chunkhash].js' // → bundle.8e0d62a03.js
}
};
Jika Anda memerlukan nama file untuk mengirimkannya ke klien, gunakan HtmlWebpackPlugin
atau WebpackManifestPlugin
.
HtmlWebpackPlugin
adalah
pendekatan yang sederhana, tetapi kurang fleksibel. Selama kompilasi, plugin ini menghasilkan
file HTML yang menyertakan semua resource yang dikompilasi. Jika logika server Anda tidak rumit, logika tersebut seharusnya cukup untuk Anda:
<!-- index.html -->
<!DOCTYPE html>
<!-- ... -->
<script src="bundle.8e0d62a03.js"></script>
WebpackManifestPlugin
adalah pendekatan lebih fleksibel yang berguna jika Anda memiliki bagian server yang kompleks.
Selama build, aplikasi akan menghasilkan file JSON dengan pemetaan antara nama file
tanpa hash dan nama file dengan hash. Gunakan JSON ini di server untuk mengetahui file mana yang akan digunakan:
// manifest.json
{
"bundle.js": "bundle.8e0d62a03.js"
}
Bacaan lebih lanjut
- Jake Archibald tentang menyimpan praktik terbaik dalam cache
Mengekstrak dependensi dan runtime ke dalam file terpisah
Dependensi
Dependensi aplikasi cenderung lebih jarang berubah daripada kode aplikasi yang sebenarnya. Jika Anda memindahkannya ke file terpisah, browser akan dapat menyimpannya dalam cache secara terpisah – dan tidak akan mendownload ulang ekstensi setiap kali kode aplikasi berubah.
Untuk mengekstrak dependensi ke dalam potongan terpisah, lakukan tiga langkah:
Ganti nama file output dengan
[name].[chunkname].js
:// webpack.config.js module.exports = { output: { // Before filename: 'bundle.[chunkhash].js', // After filename: '[name].[chunkhash].js' } };
Saat mem-build aplikasi, webpack akan mengganti
[name]
dengan nama suatu potongan. Jika tidak menambahkan bagian[name]
, kita harus membedakan setiap potongan berdasarkan hash-nya – dan ini cukup sulit.Konversi kolom
entry
menjadi objek:// webpack.config.js module.exports = { // Before entry: './index.js', // After entry: { main: './index.js' } };
Dalam cuplikan ini, "{i>main<i}" adalah nama dari sebuah potongan. Nama ini akan diganti sebagai pengganti
[name]
dari langkah 1.Sekarang, jika Anda mem-build aplikasi, bagian ini akan menyertakan seluruh kode aplikasi – sama seperti kita belum melakukan langkah-langkah ini. Namun, hal ini akan berubah dalam beberapa saat.
Di webpack 4, tambahkan opsi
optimization.splitChunks.chunks: 'all'
ke konfigurasi webpack Anda:// webpack.config.js (for webpack 4) module.exports = { optimization: { splitChunks: { chunks: 'all' } } };
Opsi ini memungkinkan pemisahan kode cerdas. Dengan layanan ini, webpack akan mengekstrak kode vendor jika ukurannya lebih besar dari 30 kB (sebelum minifikasi dan gzip). Alat ini juga akan mengekstrak kode umum – hal ini berguna jika build Anda menghasilkan beberapa paket (mis. jika Anda membagi aplikasi ke dalam beberapa rute).
Di webpack 3, tambahkan
CommonsChunkPlugin
:// webpack.config.js (for webpack 3) module.exports = { plugins: [ new webpack.optimize.CommonsChunkPlugin({ // A name of the chunk that will include the dependencies. // This name is substituted in place of [name] from step 1 name: 'vendor', // A function that determines which modules to include into this chunk minChunks: module => module.context && module.context.includes('node_modules'), }) ] };
Plugin ini mengambil semua modul yang jalurnya menyertakan
node_modules
dan memindahkannya ke file terpisah bernamavendor.[chunkhash].js
.
Setelah perubahan ini, setiap build akan menghasilkan dua file, bukan satu: main.[chunkhash].js
dan
vendor.[chunkhash].js
(vendors~main.[chunkhash].js
untuk webpack 4). Dalam kasus webpack 4,
paket vendor mungkin tidak dihasilkan jika dependensi berukuran kecil – dan itu tidak masalah:
$ webpack
Hash: ac01483e8fec1fa70676
Version: webpack 3.8.1
Time: 3816ms
Asset Size Chunks Chunk Names
./main.00bab6fd3100008a42b0.js 82 kB 0 [emitted] main
./vendor.d9e134771799ecdf9483.js 47 kB 1 [emitted] vendor
Browser akan meng-cache file tersebut secara terpisah – dan hanya mendownload ulang kode yang berubah.
Kode runtime Webpack
Sayangnya, mengekstraksi kode vendor saja tidaklah cukup. Jika Anda mencoba mengubah sesuatu dalam kode aplikasi:
// index.js
…
…
// E.g. add this:
console.log('Wat');
Anda akan melihat bahwa hash vendor
juga berubah:
Asset Size Chunks Chunk Names
./vendor.d9e134771799ecdf9483.js 47 kB 1 [emitted] vendor
↓
Asset Size Chunks Chunk Names
./vendor.e6ea4504d61a1cc1c60b.js 47 kB 1 [emitted] vendor
Hal ini terjadi karena paket webpack, terlepas dari kode modul, memiliki runtime – bagian kecil kode yang mengelola eksekusi modul. Saat Anda membagi kode menjadi beberapa file, bagian kode ini akan mulai menyertakan pemetaan antara ID potongan dan file yang sesuai:
// vendor.e6ea4504d61a1cc1c60b.js
script.src = __webpack_require__.p + chunkId + "." + {
"0": "2f2269c7f0a55a5c1871"
}[chunkId] + ".js";
Webpack menyertakan runtime ini ke dalam potongan yang terakhir dihasilkan, yaitu vendor
. Setiap kali ada potongan yang berubah, potongan kode ini juga berubah,
sehingga menyebabkan seluruh potongan vendor
berubah.
Untuk mengatasi hal ini, mari pindahkan runtime ke file terpisah. Di webpack 4, hal ini
dicapai dengan mengaktifkan opsi optimization.runtimeChunk
:
// webpack.config.js (for webpack 4)
module.exports = {
optimization: {
runtimeChunk: true
}
};
Di webpack 3, lakukan ini dengan membuat potongan kosong tambahan dengan CommonsChunkPlugin
:
// webpack.config.js (for webpack 3)
module.exports = {
plugins: [
new webpack.optimize.CommonsChunkPlugin({
name: 'vendor',
minChunks: module => module.context && module.context.includes('node_modules')
}),
// This plugin must come after the vendor one (because webpack
// includes runtime into the last chunk)
new webpack.optimize.CommonsChunkPlugin({
name: 'runtime',
// minChunks: Infinity means that no app modules
// will be included into this chunk
minChunks: Infinity
})
]
};
Setelah perubahan ini, setiap build akan menghasilkan tiga file:
$ webpack
Hash: ac01483e8fec1fa70676
Version: webpack 3.8.1
Time: 3816ms
Asset Size Chunks Chunk Names
./main.00bab6fd3100008a42b0.js 82 kB 0 [emitted] main
./vendor.26886caf15818fa82dfa.js 46 kB 1 [emitted] vendor
./runtime.79f17c27b335abc7aaf4.js 1.45 kB 3 [emitted] runtime
Sertakan ke dalam index.html
dalam urutan terbalik – dan selesai:
<!-- index.html -->
<script src="./runtime.79f17c27b335abc7aaf4.js"></script>
<script src="./vendor.26886caf15818fa82dfa.js"></script>
<script src="./main.00bab6fd3100008a42b0.js"></script>
Bacaan lebih lanjut
- Panduan Webpack tentang cache jangka panjang
- Dokumen Webpack tentang runtime dan manifes webpack
- "Mendapatkan hasil maksimal dari CommonsChunkPlugin"
- Cara kerja
optimization.splitChunks
danoptimization.runtimeChunk
Runtime webpack inline untuk menyimpan permintaan HTTP tambahan
Untuk membuat segalanya lebih baik, coba sisipkan runtime webpack ke dalam respons HTML. Dengan kata lain:
<!-- index.html -->
<script src="./runtime.79f17c27b335abc7aaf4.js"></script>
Lakukan hal ini:
<!-- index.html -->
<script>
!function(e){function n(r){if(t[r])return t[r].exports;…}} ([]);
</script>
Runtime ini berukuran kecil dan menyisipkannya akan membantu Anda menyimpan permintaan HTTP (cukup penting dengan HTTP/1; tidak terlalu penting dengan HTTP/2, tetapi mungkin masih berpengaruh).
Berikut cara melakukannya.
Jika Anda menghasilkan HTML dengan htmlWebpackPlugin
Jika Anda menggunakan HtmlWebpackPlugin untuk menghasilkan file HTML, Anda hanya membutuhkan InlineSourcePlugin:
const HtmlWebpackPlugin = require('html-webpack-plugin');
const InlineSourcePlugin = require('html-webpack-inline-source-plugin');
module.exports = {
plugins: [
new HtmlWebpackPlugin({
inlineSource: 'runtime~.+\\.js',
}),
new InlineSourcePlugin()
]
};
Jika Anda membuat HTML menggunakan logika server kustom
Dengan webpack 4:
Tambahkan
WebpackManifestPlugin
untuk mengetahui nama yang dihasilkan dari potongan runtime:// webpack.config.js (for webpack 4) const ManifestPlugin = require('webpack-manifest-plugin'); module.exports = { plugins: [ new ManifestPlugin() ] };
Build dengan plugin ini akan membuat file yang terlihat seperti ini:
// manifest.json { "runtime~main.js": "runtime~main.8e0d62a03.js" }
Sejajarkan konten potongan runtime dengan cara yang mudah. Misalnya, dengan Node.js dan Express:
// server.js const fs = require('fs'); const manifest = require('./manifest.json'); const runtimeContent = fs.readFileSync(manifest['runtime~main.js'], 'utf-8'); app.get('/', (req, res) => { res.send(` … <script>${runtimeContent}</script> … `); });
Atau dengan webpack 3:
Buat nama runtime menjadi statis dengan menetapkan
filename
:module.exports = { plugins: [ new webpack.optimize.CommonsChunkPlugin({ name: 'runtime', minChunks: Infinity, filename: 'runtime.js' }) ] };
Sejajarkan konten
runtime.js
dengan cara yang mudah. Misalnya, dengan Node.js dan Express:// server.js const fs = require('fs'); const runtimeContent = fs.readFileSync('./runtime.js', 'utf-8'); app.get('/', (req, res) => { res.send(` … <script>${runtimeContent}</script> … `); });
Kode pemuatan lambat yang tidak diperlukan saat ini
Terkadang, halaman memiliki bagian yang lebih banyak dan kurang penting:
- Jika Anda memuat halaman video di YouTube, Anda lebih peduli dengan video itu daripada komentar. Di sini, video itu lebih penting daripada komentar.
- Jika membuka artikel di situs berita, Anda lebih mementingkan teks artikel tersebut daripada iklan. Di sini, teks lebih penting daripada iklan.
Dalam kasus tersebut, tingkatkan performa pemuatan awal dengan hanya mendownload
hal yang paling penting terlebih dahulu, dan menjalankan lambat untuk memuat bagian yang tersisa nanti. Gunakan fungsi import()
dan pemisahan kode untuk ini:
// videoPlayer.js
export function renderVideoPlayer() { … }
// comments.js
export function renderComments() { … }
// index.js
import {renderVideoPlayer} from './videoPlayer';
renderVideoPlayer();
// …Custom event listener
onShowCommentsClick(() => {
import('./comments').then((comments) => {
comments.renderComments();
});
});
import()
menentukan bahwa Anda ingin memuat modul tertentu secara dinamis. Saat
melihat import('./module.js')
, webpack akan memindahkan modul ini ke bagian
terpisah:
$ webpack
Hash: 39b2a53cb4e73f0dc5b2
Version: webpack 3.8.1
Time: 4273ms
Asset Size Chunks Chunk Names
./0.8ecaf182f5c85b7a8199.js 22.5 kB 0 [emitted]
./main.f7e53d8e13e9a2745d6d.js 60 kB 1 [emitted] main
./vendor.4f14b6326a80f4752a98.js 46 kB 2 [emitted] vendor
./runtime.79f17c27b335abc7aaf4.js 1.45 kB 3 [emitted] runtime
dan mendownloadnya hanya saat eksekusi mencapai fungsi import()
.
Tindakan ini akan memperkecil paket main
, sehingga mempercepat waktu pemuatan awal.
Terlebih lagi, ini akan meningkatkan caching – jika Anda mengubah kode di potongan utama,
potongan komentar tidak akan terpengaruh.
Bacaan lebih lanjut
- Dokumen Webpack untuk fungsi
import()
- Proposal JavaScript untuk menerapkan sintaksis
import()
Bagi kode menjadi beberapa rute dan halaman
Jika aplikasi Anda memiliki beberapa rute atau halaman, tetapi hanya ada satu file JS dengan
kode tersebut (satu potongan main
), kemungkinan Anda melayani byte tambahan pada
setiap permintaan. Misalnya, saat pengguna mengunjungi halaman beranda situs Anda:
mereka tidak perlu memuat kode untuk merender artikel yang ada di halaman yang berbeda – tetapi mereka akan memuatnya. Selain itu, jika pengguna hanya selalu mengunjungi halaman beranda, dan Anda mengubah kode artikel, webpack akan membatalkan seluruh paket – dan pengguna harus mendownload ulang seluruh aplikasi.
Jika aplikasi dibagi menjadi beberapa halaman (atau rute, jika berupa aplikasi satu halaman), pengguna hanya akan mendownload kode yang relevan. Selain itu, browser akan menyimpan kode aplikasi dalam cache dengan lebih baik: jika Anda mengubah kode halaman beranda, webpack hanya akan membatalkan validasi potongan yang sesuai.
Untuk aplikasi web satu halaman
Untuk memisahkan aplikasi satu halaman menurut rute, gunakan import()
(lihat bagian “Kode pemuatan lambat
yang tidak Anda perlukan saat ini”). Jika Anda menggunakan framework,
framework tersebut mungkin sudah memiliki solusi:
- "Pemisahan
Kode"
di dokumen
react-router
(untuk React) - "Lazy Loading
Routes" di
dokumen
vue-router
(untuk Vue.js)
Untuk aplikasi multi-halaman tradisional
Untuk memisahkan aplikasi tradisional berdasarkan halaman, gunakan titik entri webpack. Jika aplikasi Anda memiliki tiga jenis halaman: halaman beranda, halaman artikel, dan halaman akun pengguna, aplikasi harus memiliki tiga entri:
// webpack.config.js
module.exports = {
entry: {
home: './src/Home/index.js',
article: './src/Article/index.js',
profile: './src/Profile/index.js'
}
};
Untuk setiap file entri, webpack akan membuat hierarki dependensi terpisah dan menghasilkan paket yang hanya menyertakan modul yang digunakan oleh entri tersebut:
$ webpack
Hash: 318d7b8490a7382bf23b
Version: webpack 3.8.1
Time: 4273ms
Asset Size Chunks Chunk Names
./0.8ecaf182f5c85b7a8199.js 22.5 kB 0 [emitted]
./home.91b9ed27366fe7e33d6a.js 18 kB 1 [emitted] home
./article.87a128755b16ac3294fd.js 32 kB 2 [emitted] article
./profile.de945dc02685f6166781.js 24 kB 3 [emitted] profile
./vendor.4f14b6326a80f4752a98.js 46 kB 4 [emitted] vendor
./runtime.318d7b8490a7382bf23b.js 1.45 kB 5 [emitted] runtime
Jadi, jika hanya halaman artikel yang menggunakan Lodash, paket home
dan profile
tidak akan menyertakannya – dan pengguna tidak perlu mendownload library ini saat
mengunjungi halaman beranda.
Namun, hierarki dependensi terpisah memiliki kekurangan. Jika dua titik entri menggunakan
Lodash, dan Anda belum memindahkan dependensi ke dalam paket vendor, kedua titik
entri tersebut akan menyertakan salinan Lodash. Untuk mengatasi hal ini, di webpack 4, tambahkan opsi
optimization.splitChunks.chunks: 'all'
ke konfigurasi webpack Anda:
// webpack.config.js (for webpack 4)
module.exports = {
optimization: {
splitChunks: {
chunks: 'all'
}
}
};
Opsi ini memungkinkan pemisahan kode cerdas. Dengan opsi ini, webpack akan otomatis mencari kode umum dan mengekstraknya ke dalam file terpisah.
Atau, di webpack 3, gunakan CommonsChunkPlugin
– ini akan memindahkan dependensi umum ke dalam file tertentu yang baru:
module.exports = {
plugins: [
new webpack.optimize.CommonsChunkPlugin({
name: 'common',
minChunks: 2 // 2 is the default value
})
]
};
Jangan ragu untuk bermain dengan nilai minChunks
untuk menemukan yang terbaik. Umumnya,
Anda ingin membuatnya tetap kecil, tetapi meningkatkannya jika jumlah bagiannya bertambah. Misalnya, untuk 3 bagian, minChunks
mungkin 2, tetapi untuk 30 potongan, mungkin 8
– karena jika Anda menyimpannya di 2, terlalu banyak modul akan masuk ke file umum,
sehingga meng-inflate terlalu banyak.
Bacaan lebih lanjut
- Dokumen Webpack tentang konsep titik masuk
- Dokumen Webpack tentang CommonsChunkPlugin
- "Mendapatkan hasil maksimal dari CommonsChunkPlugin"
- Cara kerja
optimization.splitChunks
danoptimization.runtimeChunk
Membuat ID modul lebih stabil
Saat membuat kode, webpack menetapkan ID untuk setiap modul. Kemudian, ID ini akan digunakan dalam require()
di dalam paket. Anda biasanya melihat ID dalam output build
tepat sebelum jalur modul:
$ webpack
Hash: df3474e4f76528e3bbc9
Version: webpack 3.8.1
Time: 2150ms
Asset Size Chunks Chunk Names
./0.8ecaf182f5c85b7a8199.js 22.5 kB 0 [emitted]
./main.4e50a16675574df6a9e9.js 60 kB 1 [emitted] main
./vendor.26886caf15818fa82dfa.js 46 kB 2 [emitted] vendor
./runtime.79f17c27b335abc7aaf4.js 1.45 kB 3 [emitted] runtime
↓ Di sini
[0] ./index.js 29 kB {1} [built]
[2] (webpack)/buildin/global.js 488 bytes {2} [built]
[3] (webpack)/buildin/module.js 495 bytes {2} [built]
[4] ./comments.js 58 kB {0} [built]
[5] ./ads.js 74 kB {1} [built]
+ 1 hidden module
Secara default, ID dihitung menggunakan penghitung (yaitu modul pertama memiliki ID 0, modul kedua memiliki ID 1, dan seterusnya). Masalahnya adalah saat Anda menambahkan modul baru, modul tersebut mungkin muncul di tengah daftar modul, sehingga mengubah semua ID modul berikutnya:
$ webpack
Hash: df3474e4f76528e3bbc9
Version: webpack 3.8.1
Time: 2150ms
Asset Size Chunks Chunk Names
./0.5c82c0f337fcb22672b5.js 22 kB 0 [emitted]
./main.0c8b617dfc40c2827ae3.js 82 kB 1 [emitted] main
./vendor.26886caf15818fa82dfa.js 46 kB 2 [emitted] vendor
./runtime.79f17c27b335abc7aaf4.js 1.45 kB 3 [emitted] runtime
[0] ./index.js 29 kB {1} [built]
[2] (webpack)/buildin/global.js 488 bytes {2} [built]
[3] (webpack)/buildin/module.js 495 bytes {2} [built]
↓ Kami telah menambahkan modul baru...
[4] ./webPlayer.js 24 kB {1} [built]
↓ Dan lihat apa yang telah dilakukannya! comments.js
kini memiliki ID 5, bukan 4
[5] ./comments.js 58 kB {0} [built]
↓ ads.js
kini memiliki ID 6, bukan 5
[6] ./ads.js 74 kB {1} [built]
+ 1 hidden module
Tindakan ini akan membatalkan semua bagian yang menyertakan atau bergantung pada modul dengan ID yang diubah –
meskipun kode sebenarnya tidak berubah. Dalam kasus kita, potongan 0
(potongan
dengan comments.js
) dan potongan main
(potongan dengan kode aplikasi lainnya)
dibatalkan; sedangkan hanya main
yang seharusnya.
Untuk mengatasi hal ini, ubah cara ID modul dihitung menggunakan
HashedModuleIdsPlugin
.
Fungsi ini menggantikan ID berbasis penghitung dengan hash jalur modul:
$ webpack
Hash: df3474e4f76528e3bbc9
Version: webpack 3.8.1
Time: 2150ms
Asset Size Chunks Chunk Names
./0.6168aaac8461862eab7a.js 22.5 kB 0 [emitted]
./main.a2e49a279552980e3b91.js 60 kB 1 [emitted] main
./vendor.ff9f7ea865884e6a84c8.js 46 kB 2 [emitted] vendor
./runtime.25f5d0204e4f77fa57a1.js 1.45 kB 3 [emitted] runtime
↓ Di sini
[3IRH] ./index.js 29 kB {1} [built]
[DuR2] (webpack)/buildin/global.js 488 bytes {2} [built]
[JkW7] (webpack)/buildin/module.js 495 bytes {2} [built]
[LbCc] ./webPlayer.js 24 kB {1} [built]
[lebJ] ./comments.js 58 kB {0} [built]
[02Tr] ./ads.js 74 kB {1} [built]
+ 1 hidden module
Dengan pendekatan ini, ID modul hanya berubah jika Anda mengganti nama atau memindahkan modul tersebut. Modul baru tidak akan memengaruhi ID modul lain.
Untuk mengaktifkan plugin, tambahkan ke bagian plugins
pada konfigurasi:
// webpack.config.js
module.exports = {
plugins: [
new webpack.HashedModuleIdsPlugin()
]
};
Bacaan lebih lanjut
- Dokumentasi Webpack tentang HashedModuleIdsPlugin
Mengambil kesimpulan
- Menyimpan paket dalam cache dan membedakan setiap versi dengan mengubah nama paket
- Bagi paket menjadi kode aplikasi, kode vendor, dan runtime
- Membuat runtime runtime untuk menyimpan permintaan HTTP
- Pemuatan lambat kode non-kritis dengan
import
- Memisahkan kode menurut rute/halaman untuk menghindari pemuatan hal-hal yang tidak perlu