Memanfaatkan cache jangka panjang

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:

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

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

Mengekstrak dependensi dan runtime ke 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:

  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 mengganti [name] dengan nama potongan. Jika tidak menambahkan bagian [name], kita harus membedakan antara potongan berdasarkan hash-nya – yang cukup sulit.

  2. 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 dengan [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, perubahan ini akan segera terjadi.

  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 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 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 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, terlepas dari 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 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 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

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

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

Runtime kecil, dan membuat runtime akan membantu Anda menyimpan permintaan HTTP (cukup penting dengan HTTP/1; kurang penting pada HTTP/2 tetapi mungkin masih berpengaruh).

Berikut cara melakukannya.

Jika Anda membuat HTML dengan HtmlWebpackPlugin

Jika Anda menggunakan HtmlWebpackPlugin untuk menghasilkan file HTML, Anda hanya memerlukan 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. Buat konten potongan runtime secara inline 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 menentukan filename:

    module.exports = {
      plugins: [
        new webpack.optimize.CommonsChunkPlugin({
          name: 'runtime',
          minChunks: Infinity,
          filename: 'runtime.js'
        })
      ]
    };
    
  2. 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 penting 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 semacam ini, tingkatkan performa pemuatan awal dengan hanya mendownload hal yang paling penting terlebih dahulu, dan memuat bagian lainnya secara lambat nanti. Gunakan fungsi import() dan code-splitting 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, hal ini akan meningkatkan kualitas cache – jika Anda mengubah kode di bagian 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 bagian main), kemungkinan Anda menayangkan byte tambahan pada setiap permintaan. Misalnya, saat pengguna mengunjungi halaman beranda situs Anda:

Halaman beranda WebFundamentals

kode tersebut tidak perlu memuat kode untuk merender artikel yang ada di halaman lain, tetapi kode tersebut akan memuat kode tersebut. 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 berupa 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, mungkin sudah ada solusi untuk hal ini:

Untuk aplikasi multi-halaman tradisional

Untuk memisahkan aplikasi tradisional menurut 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 membangun 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 yang terpisah memiliki kelemahan. Jika dua titik entri menggunakan Lodash, dan Anda belum memindahkan dependensi ke dalam paket vendor, kedua titik entri 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 pintar. Dengan opsi ini, webpack akan otomatis mencari kode 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
    })
  ]
};

Anda dapat mengubah nilai minChunks untuk menemukan nilai terbaik. Umumnya, Anda perlu mempertahankannya kecil, tetapi bertambah jika jumlah potongannya 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

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 tidak 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 plugin ke bagian plugins pada konfigurasi:

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

Bacaan lebih lanjut

Merangkum

  • Menyimpan cache paket dan membedakan antara versi dengan mengubah nama paket
  • Bagi paket menjadi kode aplikasi, kode vendor, dan runtime
  • Membuat runtime inline untuk menyimpan permintaan HTTP
  • Memuat lambat kode non-kritis dengan import
  • Pisahkan kode berdasarkan rute/halaman untuk menghindari pemuatan hal-hal yang tidak perlu