Cara webpack membantu penyimpanan dalam cache aset
Hal berikutnya (setelah mengoptimalkan ukuran aplikasi yang meningkatkan waktu pemuatan aplikasi adalah penyimpanan dalam cache. Gunakan untuk menyimpan bagian aplikasi di klien dan menghindari mendownload ulang setiap saat.
Menggunakan header cache dan pembuatan versi paket
Pendekatan umum untuk melakukan penyimpanan dalam cache adalah dengan:
memberi tahu browser untuk meng-cache file dalam waktu yang sangat lama (misalnya, satu tahun):
# Server header Cache-Control: max-age=31536000
Jika Anda tidak memahami fungsi
Cache-Control
, lihat postingan bagus dari Jake Archibald 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 memberi tahu browser untuk mendownload file JS, menyimpannya dalam cache, dan menggunakan salinan yang di-cache. Browser hanya akan mengakses jaringan jika nama file berubah (atau jika satu tahun berlalu).
Dengan webpack, Anda melakukan hal yang sama, tetapi sebagai ganti nomor versi, Anda menentukan
hash file. 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
kompleks, hal ini seharusnya cukup bagi Anda:
<!-- index.html -->
<!DOCTYPE html>
<!-- ... -->
<script src="bundle.8e0d62a03.js"></script>
WebpackManifestPlugin
adalah pendekatan yang lebih fleksibel yang berguna jika Anda memiliki bagian server yang kompleks.
Selama build, file JSON akan dihasilkan 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 praktik terbaik caching
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 mendownloadnya lagi setiap kali kode aplikasi berubah.
Untuk mengekstrak dependensi ke dalam bagian 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 bagian. Jika tidak menambahkan bagian[name]
, kita harus membedakan antara potongan berdasarkan hash-nya – yang cukup sulit.Konversikan kolom
entry
menjadi objek:// webpack.config.js module.exports = { // Before entry: './index.js', // After entry: { main: './index.js' } };
Dalam cuplikan ini, "main" adalah nama bagian. Nama ini akan diganti sebagai pengganti
[name]
dari langkah 1.Sekarang, jika Anda mem-build aplikasi, bagian ini akan menyertakan seluruh kode aplikasi – seperti kalau kita belum melakukan langkah-langkah ini. Namun, hal ini akan berubah dalam hitungan detik.
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 pintar. Dengannya, webpack akan mengekstrak kode vendor jika ukuran kode tersebut lebih besar dari 30 kB (sebelum minifikasi dan gzip). Tindakan ini juga akan mengekstrak kode umum – hal ini berguna jika build Anda menghasilkan beberapa paket (misalnya, jika Anda membagi aplikasi menjadi 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 yang disebutvendor.[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). Untuk webpack 4,
paket vendor mungkin tidak dibuat jika dependensinya 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 ini secara terpisah – dan hanya mendownload ulang kode yang berubah.
Kode runtime Webpack
Sayangnya, mengekstrak kode vendor saja tidak 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, selain kode modul, memiliki runtime – potongan kecil kode yang mengelola eksekusi modul. Saat Anda membagi kode menjadi beberapa file, bagian kode ini mulai menyertakan pemetaan antara ID bagian dan file yang sesuai:
// vendor.e6ea4504d61a1cc1c60b.js
script.src = __webpack_require__.p + chunkId + "." + {
"0": "2f2269c7f0a55a5c1871"
}[chunkId] + ".js";
Webpack menyertakan runtime ini ke dalam bagian terakhir yang dihasilkan, yaitu vendor
dalam kasus kita. Dan setiap kali ada bagian yang berubah, bagian kode ini juga akan berubah,
sehingga menyebabkan seluruh bagian vendor
berubah.
Untuk mengatasinya, mari kita 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 bagian 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 Anda sudah 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 caching 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 membuatnya lebih baik, coba sisipkan runtime webpack ke dalam respons HTML. Artinya, bukan seperti ini:
<!-- index.html -->
<script src="./runtime.79f17c27b335abc7aaf4.js"></script>
lakukan hal berikut:
<!-- index.html -->
<script>
!function(e){function n(r){if(t[r])return t[r].exports;…}} ([]);
</script>
Runtime-nya kecil, dan menyisipkannya akan membantu Anda menyimpan permintaan HTTP (cukup penting dengan HTTP/1; kurang penting dengan HTTP/2, tetapi mungkin masih memainkan efek).
Berikut cara melakukannya.
Jika Anda membuat HTML dengan HtmlWebpackPlugin
Jika Anda menggunakan HtmlWebpackPlugin untuk membuat file HTML, InlineSourcePlugin adalah yang Anda butuhkan:
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" }
Menyisipkan 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 menentukan
filename
:module.exports = { plugins: [ new webpack.optimize.CommonsChunkPlugin({ name: 'runtime', minChunks: Infinity, filename: 'runtime.js' }) ] };
Gabungkan 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 Anda perlukan saat ini
Terkadang, halaman memiliki bagian yang lebih banyak dan kurang penting:
- Jika memuat halaman video di YouTube, Anda lebih tertarik dengan video daripada komentar. Di sini, video lebih penting daripada komentar.
- Jika membuka artikel di situs berita, Anda lebih memperhatikan teks artikel daripada iklan. Di sini, teks lebih penting daripada iklan.
Dalam kasus tersebut, tingkatkan performa pemuatan awal dengan hanya mendownload
hal-hal yang paling penting terlebih dahulu, dan memuat lambat 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 membuat paket main
lebih kecil, sehingga meningkatkan waktu pemuatan awal.
Selain itu, hal ini akan meningkatkan kualitas penyimpanan dalam cache – jika Anda mengubah kode di bagian utama,
bagian komentar tidak akan terpengaruh.
Bacaan lebih lanjut
- Dokumen Webpack untuk fungsi
import()
- Proposal JavaScript untuk menerapkan sintaksis
import()
Memisahkan kode menjadi rute dan halaman
Jika aplikasi Anda memiliki beberapa rute atau halaman, tetapi hanya ada satu file JS dengan
kode (satu bagian main
), kemungkinan Anda menayangkan byte tambahan pada
setiap permintaan. Misalnya, saat pengguna mengunjungi halaman beranda situs Anda:
kode tersebut tidak perlu memuat kode untuk merender artikel yang ada di halaman lain, tetapi kode tersebut akan memuatnya. Selain itu, jika pengguna selalu hanya mengunjungi halaman beranda, dan Anda membuat perubahan pada kode artikel, webpack akan membatalkan seluruh paket – dan pengguna harus mendownload ulang seluruh aplikasi.
Jika kita membagi aplikasi menjadi beberapa halaman (atau rute, jika aplikasi tersebut adalah aplikasi satu halaman), pengguna hanya akan mendownload kode yang relevan. Selain itu, browser akan meng-cache kode aplikasi dengan lebih baik: jika Anda mengubah kode halaman beranda, webpack hanya akan membatalkan bagian yang sesuai.
Untuk aplikasi web satu halaman
Untuk membagi 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 untuk hal ini:
- "Pembagian
Kode"
dalam dokumen
react-router
(untuk React) - "Lazy Loading Route" dalam dokumen
vue-router
(untuk Vue.js)
Untuk aplikasi multi-halaman tradisional
Untuk membagi aplikasi tradisional berdasarkan halaman, gunakan titik entri webpack. Jika aplikasi Anda memiliki tiga jenis halaman: halaman beranda, halaman artikel, dan halaman akun pengguna, aplikasi tersebut 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 mem-build hierarki dependensi terpisah dan membuat 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 kelemahannya. Jika dua titik entri menggunakan
Lodash, dan Anda belum memindahkan dependensi ke dalam paket vendor, kedua titik
entri akan menyertakan salinan Lodash. Untuk mengatasinya, 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 pintar. Dengan opsi ini, webpack akan otomatis mencari kode umum dan mengekstraknya ke dalam file terpisah.
Atau, di webpack 3, gunakan CommonsChunkPlugin
– tindakan ini akan memindahkan dependensi umum ke file baru yang ditentukan:
module.exports = {
plugins: [
new webpack.optimize.CommonsChunkPlugin({
name: 'common',
minChunks: 2 // 2 is the default value
})
]
};
Anda dapat mengubah nilai minChunks
untuk menemukan nilai terbaik. Umumnya,
Anda ingin membuatnya tetap kecil, tetapi bertambah jika jumlah potongan bertambah. Misalnya, untuk 3 bagian, minChunks
mungkin 2, tetapi untuk 30 bagian, mungkin 8
– karena jika Anda tetap mempertahankannya pada 2, terlalu banyak modul yang akan masuk ke file umum,
sehingga terlalu membengkak.
Bacaan lebih lanjut
- Dokumen Webpack tentang konsep titik entri
- Dokumen Webpack tentang CommonsChunkPlugin
- "Mendapatkan hasil maksimal dari CommonsChunkPlugin"
- Cara kerja
optimization.splitChunks
danoptimization.runtimeChunk
Membuat ID modul lebih stabil
Saat mem-build kode, webpack menetapkan ID ke setiap modul. Kemudian, ID ini
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]
↓ 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 belum berubah. Dalam kasus ini, bagian 0
(bagian
dengan comments.js
) dan bagian main
(bagian dengan kode aplikasi lainnya) menjadi
tidak valid – sedangkan seharusnya hanya bagian main
yang tidak valid.
Untuk mengatasinya, ubah cara penghitungan ID modul menggunakan
HashedModuleIdsPlugin
.
ID 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
konfigurasi:
// webpack.config.js
module.exports = {
plugins: [
new webpack.HashedModuleIdsPlugin()
]
};
Bacaan lebih lanjut
- Dokumen Webpack tentang HashedModuleIdsPlugin
Merangkum
- Menyimpan cache paket dan membedakan antara versi dengan mengubah nama paket
- Membagi paket menjadi kode aplikasi, kode vendor, dan runtime
- Menyertakan runtime untuk menyimpan permintaan HTTP
- Memuat lambat kode non-kritis dengan
import
- Memisahkan kode menurut rute/halaman untuk menghindari pemuatan hal yang tidak perlu