Memanfaatkan cache jangka panjang

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:

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

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

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:

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

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

  3. 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 bernama 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 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

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:

  1. 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"
    }
    
  2. 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:

  1. Buat nama runtime menjadi statis dengan menetapkan filename:

    module.exports = {
      plugins: [
        new webpack.optimize.CommonsChunkPlugin({
          name: 'runtime',
          minChunks: Infinity,
          filename: 'runtime.js'
        })
      ]
    };
    
  2. 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

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:

Beranda WebFundamentals

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:

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

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

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