Memanfaatkan cache jangka panjang

Cara webpack membantu penyimpanan aset dalam cache

Hal berikutnya (setelah mengoptimalkan ukuran aplikasi yang meningkatkan waktu pemuatan aplikasi dalam cache. Gunakan untuk menyimpan bagian-bagian aplikasi di klien dan menghindari mengunduhnya ulang setiap saat.

Menggunakan pembuatan versi paket dan header cache

Pendekatan umum dalam cache adalah:

  1. beri tahu browser untuk meng-cache file dalam waktu yang sangat lama (misalnya, setahun):

    # Server header
    Cache-Control: max-age=31536000
    

    Jika Anda tidak tahu apa yang dilakukan Cache-Control, lihat postingan yang sangat baik tentang penyimpanan cache yang terbaik praktik.

  2. 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 memberitahu browser untuk mengunduh file JS, menyimpannya dalam cache, dan menggunakan salinan yang di-cache. Browser hanya akan masuk ke jaringan hanya jika nama file berubah (atau jika satu tahun berlalu).

Dengan webpack, Anda melakukan hal yang sama, tetapi alih-alih menggunakan nomor versi, Anda menentukan {i>hash <i}file. Untuk menyertakan {i>hash<i} 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 untuk mengirimkannya ke klien, gunakan HtmlWebpackPlugin atau WebpackManifestPlugin.

HtmlWebpackPlugin adalah pendekatan yang sederhana, tetapi kurang fleksibel. Selama kompilasi, plugin ini menghasilkan file HTML yang mencakup semua sumber daya yang dikompilasi. Jika logika server Anda tidak rumit, maka cukup bagi Anda:

<!-- index.html -->
<!DOCTYPE html>
<!-- ... -->
<script src="bundle.8e0d62a03.js"></script>

Tujuan WebpackManifestPlugin adalah pendekatan yang lebih fleksibel yang berguna jika Anda memiliki bagian server yang kompleks. Selama proses build, build ini menghasilkan file JSON dengan pemetaan antarnama file tanpa {i>hash<i} dan nama file dengan {i>hash<i}. Gunakan JSON ini pada server untuk mengetahui file yang akan dikerjakan:

// manifest.json
{
  "bundle.js": "bundle.8e0d62a03.js"
}

Bacaan lebih lanjut

Mengekstrak dependensi dan runtime ke file terpisah

Dependensi

Dependensi aplikasi cenderung lebih jarang berubah daripada kode aplikasi yang sebenarnya. Jika Anda pindah file tersebut menjadi file terpisah, browser akan dapat menyimpannya di cache secara terpisah – dan tidak akan mengunduhnya ulang setiap kali kode aplikasi berubah.

Untuk mengekstrak dependensi ke dalam potongan terpisah, lakukan tiga langkah:

  1. 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 membangun aplikasi, webpack akan menggantikan [name] dengan nama suatu potongan. Jika kita tidak menambahkan bagian [name], kita akan memiliki membedakan antara potongan berdasarkan {i>hash<i} - yang cukup sulit!

  2. Konversi kolom entry menjadi objek:

    // webpack.config.js
    module.exports = {
      // Before
      entry: './index.js',
      // After
      entry: {
        main: './index.js'
      }
    };
    

    Dalam cuplikan ini, "main" merupakan nama dari sebuah potongan data. Nama ini akan diganti dengan posisi [name] dari langkah 1.

    Sekarang, jika Anda membangun aplikasi, potongan ini akan mencakup seluruh kode aplikasi – sepertinya kami belum melakukan langkah-langkah ini. Namun, perubahan ini akan segera terjadi.

  3. Di webpack 4, tambahkan opsi optimization.splitChunks.chunks: 'all' ke dalam konfigurasi webpack Anda:

    // webpack.config.js (for webpack 4)
    module.exports = {
      optimization: {
        splitChunks: {
          chunks: 'all'
        }
      }
    };
    

    Opsi ini mengaktifkan pemisahan kode cerdas. Dengan {i>webpack<i} itu, webpack akan mengekstrak kode vendor jika file menjadi lebih besar dari 30 kB (sebelum minifikasi dan gzip). Tindakan ini juga akan mengekstrak kode umum – berguna jika build Anda menghasilkan beberapa paket (mis. jika Anda memisahkan 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 yang disebut vendor.[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 dibuat jika dependensinya kecil – dan itu tidak apa-apa:

$ 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 ubah sesuatu di 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 sebuah runtime – kode pendek yang mengelola eksekusi modul. Ketika Anda membagi kode menjadi beberapa file, potongan kode ini mulai termasuk pemetaan antara beberapa ID dan file yang sesuai:

// vendor.e6ea4504d61a1cc1c60b.js
script.src = __webpack_require__.p + chunkId + "." + {
    "0": "2f2269c7f0a55a5c1871"
}[chunkId] + ".js";

Webpack menyertakan runtime ini ke dalam potongan terakhir yang dihasilkan, yaitu vendor dalam kasus kita. Dan setiap kali potongan apa pun berubah, potongan kode ini juga berubah, yang menyebabkan seluruh potongan vendor berubah.

Untuk mengatasi hal ini, mari pindahkan runtime ke file terpisah. Di webpack 4, ini adalah dicapai dengan mengaktifkan opsi optimization.runtimeChunk:

// webpack.config.js (for webpack 4)
module.exports = {
  optimization: {
    runtimeChunk: true
  }
};

Di webpack 3, lakukan hal ini dengan membuat potongan ekstra kosong 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 semuanya 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

Runtime webpack inline untuk menyimpan permintaan HTTP tambahan

Agar lebih baik lagi, coba sisipkan runtime webpack ke dalam HTML yang dihasilkan. Yaitu, alih-alih yang berikut:

<!-- index.html -->
<script src="./runtime.79f17c27b335abc7aaf4.js"></script>

lakukan ini:

<!-- index.html -->
<script>
!function(e){function n(r){if(t[r])return t[r].exports;…}} ([]);
</script>

Waktu proses{i> <i}ini kecil, dan menjadi {i>inline<i} akan membantu Anda menyimpan permintaan HTTP (cukup penting untuk HTTP/1; kurang penting dengan HTTP/2 tetapi mungkin masih memainkan ).

Berikut cara melakukannya.

Jika Anda membuat HTML dengan HTMLWebpackPlugin

Jika Anda menggunakan HtmlWebpackPlugin yang akan dibuat sebuah {i>file<i} HTML, InlineSourcePlugin adalah hal 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:

  1. Tambahkan WebpackManifestPlugin untuk mengetahui nama yang dihasilkan dari potongan {i>runtime<i}:

    // 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"
    }
    
  2. Buat konten potongan runtime secara inline dengan cara yang mudah. Mis. 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:

  1. Buat nama runtime statis dengan menentukan filename:

    module.exports = {
      plugins: [
        new webpack.optimize.CommonsChunkPlugin({
          name: 'runtime',
          minChunks: Infinity,
          filename: 'runtime.js'
        })
      ]
    };
    
  2. Buat konten runtime.js menjadi inline dengan cara yang mudah. Mis. 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>
        …
      `);
    });
    

Pemuatan lambat kode yang tidak diperlukan saat ini

Terkadang, halaman memiliki bagian yang lebih penting dan kurang penting:

  • Jika Anda memuat laman video di YouTube, Anda lebih peduli terhadap video itu daripada tentang komentar. Di sini, video lebih penting daripada komentar.
  • Jika Anda membuka artikel di situs berita, Anda akan lebih memperhatikan teks artikel, bukan tentang iklan. Di sini, teks lebih penting daripada iklan.

Jika demikian, tingkatkan performa pemuatan awal dengan hanya mendownload hal terpenting terlebih dahulu, lalu pemuatan lambat untuk bagian lainnya. Gunakan fungsi import() dan code-splitting untuk hal 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. Kapan webpack melihat import('./module.js'), yang akan memindahkan modul ini ke dalam chunk:

$ 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 kualitas {i>caching<i} - jika Anda mengubah kode di potongan utama, potongan komentar tidak akan terpengaruh.

Bacaan lebih lanjut

Bagi kode menjadi rute dan halaman

Jika aplikasi Anda memiliki beberapa rute atau halaman, tetapi hanya ada satu file JS dengan kode (satu potongan main), kemungkinan Anda menyajikan byte tambahan setiap permintaan. Misalnya, saat pengguna mengunjungi halaman beranda situs Anda:

Halaman beranda WebFundamentals

mereka tidak perlu memuat kode untuk merender artikel yang berada di tetapi mereka akan memuatnya. Selain itu, jika pengguna selalu hanya mengunjungi rumah 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 berupa aplikasi satu halaman), pengguna hanya akan mendownload kode yang relevan. Ditambah lagi, browser akan meng-cache kode aplikasi lebih baik: jika Anda mengubah kode halaman beranda, webpack hanya akan membatalkan potongan yang sesuai.

Untuk aplikasi web satu halaman

Untuk memisahkan aplikasi satu halaman berdasarkan rute, gunakan import() (lihat “Kode pemuatan lambat yang tidak Anda perlukan saat ini”). Jika Anda menggunakan kerangka kerja, mungkin solusi yang sudah ada untuk ini:

Untuk aplikasi multi-halaman tradisional

Untuk memisahkan aplikasi tradisional berdasarkan halaman, gunakan entri webpack poin. Jika aplikasi Anda memiliki tiga jenis halaman: halaman beranda, halaman artikel, dan halaman akun pengguna, 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 membangun hierarki dependensi terpisah dan menghasilkan paket yang hanya mencakup 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 mengunduh pustaka ini jika mengunjungi laman beranda.

Namun, hierarki dependensi yang terpisah memiliki kelemahan. Jika dua titik entri menggunakan Lodash, dan Anda belum memindahkan dependensi Anda ke dalam paket vendor, keduanya akan menyertakan salinan Lodash. Untuk mengatasinya, di webpack 4, tambahkan optimization.splitChunks.chunks: 'all' ke konfigurasi webpack Anda:

// webpack.config.js (for webpack 4)
module.exports = {
  optimization: {
    splitChunks: {
      chunks: 'all'
    }
  }
};

Opsi ini mengaktifkan pemisahan kode cerdas. Dengan opsi ini, webpack akan secara otomatis mencari kode yang umum dan mengekstraknya ke dalam file terpisah.

Atau, di webpack 3, gunakan CommonsChunkPlugin – perintah 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
    })
  ]
};

Jangan ragu untuk bermain-main dengan nilai minChunks untuk menemukan yang terbaik. Umumnya, Anda ingin membuatnya kecil, tetapi tambah jika jumlah potongannya bertambah. Sebagai misalnya, untuk 3 potongan, minChunks mungkin 2, tetapi untuk 30 potongan, mungkin 8 - karena jika Anda menyimpannya di angka 2, terlalu banyak modul yang akan masuk ke file umum, menggelembungkannya terlalu banyak.

Bacaan lebih lanjut

Membuat ID modul lebih stabil

Saat membuat kode, webpack menetapkan ID untuk setiap modul. Nantinya, 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, yang kedua memiliki ID 1, dan seterusnya). Masalahnya adalah ketika Anda menambahkan modul baru, mungkin akan muncul di tengah daftar modul, yang mengubah semua modul berikutnya' ID:

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

[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

Ini membatalkan semua bagian yang menyertakan atau bergantung pada modul dengan ID yang diubah – bahkan jika kode mereka yang sebenarnya tidak berubah. Dalam kasus kita, potongan 0 (potongan dengan comments.js) dan potongan main (potongan dengan kode aplikasi lainnya) mendapatkan tidak valid – sedangkan hanya main yang seharusnya.

Untuk mengatasi 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 ruang lingkup modul ini. Modul baru tidak akan memengaruhi modul lain pelanggan.

Untuk mengaktifkan plugin, tambahkan ke bagian plugins pada konfigurasi:

// webpack.config.js
module.exports = {
  plugins: [
    new webpack.HashedModuleIdsPlugin()
  ]
};

Bacaan lebih lanjut

Mengambil kesimpulan

  • Simpan paket dalam cache dan bedakan antar-versi dengan mengubah nama paket
  • Bagi paket menjadi kode aplikasi, kode vendor, dan runtime
  • Membuat runtime inline untuk menyimpan permintaan HTTP
  • Pemuatan lambat kode tidak penting dengan import
  • Pisahkan kode berdasarkan rute/halaman untuk menghindari pemuatan hal-hal yang tidak perlu