Meningkatkan performa pemuatan halaman Next.js dan Gatsby dengan pemotongan terperinci

Strategi chunking webpack yang lebih baru di Next.js dan Gatsby meminimalkan kode duplikat untuk meningkatkan performa pemuatan halaman.

Chrome berkolaborasi dengan alat dan framework dalam ekosistem open source JavaScript. Sejumlah pengoptimalan yang lebih baru baru-baru ini ditambahkan untuk meningkatkan performa pemuatan Next.js dan Gatsby. Artikel ini membahas strategi pengelompokan terperinci yang ditingkatkan yang kini dikirimkan secara default di kedua framework.

Pengantar

Seperti banyak framework web, Next.js dan Gatsby menggunakan webpack sebagai pemaket intinya. webpack v3 memperkenalkan CommonsChunkPlugin untuk memungkinkan modul output yang dibagikan di antara titik entri yang berbeda dalam satu (atau beberapa) potongan (atau beberapa potongan) "commons". Kode bersama dapat didownload secara terpisah dan disimpan di cache browser sejak awal, yang dapat menghasilkan performa pemuatan yang lebih baik.

Pola ini menjadi populer dengan banyak framework aplikasi web satu halaman yang mengadopsi konfigurasi titik entri dan paket yang terlihat seperti ini:

Konfigurasi entrypoint dan paket umum

Meskipun praktis, konsep penggabungan semua kode modul bersama ke dalam satu potongan memiliki batasan. Modul yang tidak dibagikan di setiap titik entri dapat didownload untuk rute yang tidak menggunakannya, sehingga lebih banyak kode yang didownload daripada yang diperlukan. Misalnya, saat page1 memuat chunk common, kode untuk moduleC akan dimuat meskipun page1 tidak menggunakan moduleC. Karena alasan ini, bersama dengan beberapa alasan lainnya, webpack v4 menghapus plugin tersebut dan menggantinya dengan plugin baru: SplitChunksPlugin.

Peningkatan chunking

Setelan default untuk SplitChunksPlugin berfungsi dengan baik bagi sebagian besar pengguna. Beberapa potongan terpisah dibuat bergantung pada sejumlah kondisi untuk mencegah pengambilan kode duplikat di beberapa rute.

Namun, banyak framework web yang menggunakan plugin ini masih mengikuti pendekatan "single-commons" untuk pemisahan chunk. Misalnya, Next.js akan menghasilkan paket commons yang berisi modul apa pun yang digunakan di lebih dari 50% halaman dan semua dependensi framework (react, react-dom, dan sebagainya).

const splitChunksConfigs = {
  …
  prod: {
    chunks: 'all',
    cacheGroups: {
      default: false,
      vendors: false,
      commons: {
        name: 'commons',
        chunks: 'all',
        minChunks: totalPages > 2 ? totalPages * 0.5 : 2,
      },
      react: {
        name: 'commons',
        chunks: 'all',
        test: /[\\/]node_modules[\\/](react|react-dom|scheduler|use-subscription)[\\/]/,
      },
    },
  },

Meskipun menyertakan kode yang bergantung pada framework ke dalam potongan bersama berarti kode tersebut dapat didownload dan di-cache untuk titik entri apa pun, heuristik berbasis penggunaan yang menyertakan modul umum yang digunakan di lebih dari setengah halaman tidak terlalu efektif. Mengubah rasio ini hanya akan menghasilkan salah satu dari dua hasil:

  • Jika Anda mengurangi rasio, lebih banyak kode yang tidak diperlukan akan didownload.
  • Jika Anda meningkatkan rasio, lebih banyak kode akan diduplikasi di beberapa rute.

Untuk mengatasi masalah ini, Next.js mengadopsi konfigurasi yang berbeda untukSplitChunksPlugin yang mengurangi kode yang tidak diperlukan untuk rute apa pun.

  • Setiap modul pihak ketiga yang cukup besar (lebih dari 160 KB) dibagi menjadi bagian tersendiri
  • Chunk frameworks terpisah dibuat untuk dependensi framework (react, react-dom, dan sebagainya)
  • Membuat sebanyak mungkin potongan bersama yang diperlukan (hingga 25)
  • Ukuran minimum untuk pembuatan potongan diubah menjadi 20 KB

Strategi pengelompokan terperinci ini memberikan manfaat berikut:

  • Waktu muat halaman ditingkatkan. Memancarkan beberapa potongan bersama, bukan satu, meminimalkan jumlah kode yang tidak diperlukan (atau duplikat) untuk titik entri apa pun.
  • Peningkatan caching selama navigasi. Membagi library besar dan dependensi framework menjadi beberapa bagian terpisah akan mengurangi kemungkinan invalidasi cache karena keduanya tidak mungkin berubah hingga upgrade dilakukan.

Anda dapat melihat seluruh konfigurasi yang diadopsi Next.js di webpack-config.ts.

Permintaan HTTP lainnya

SplitChunksPlugin menentukan dasar untuk pengelompokan terperinci, dan menerapkan pendekatan ini ke framework seperti Next.js bukanlah konsep yang sepenuhnya baru. Namun, banyak framework masih terus menggunakan strategi paket heuristik tunggal dan "commons" karena beberapa alasan. Hal ini mencakup kekhawatiran bahwa banyak permintaan HTTP lainnya dapat berdampak negatif pada performa situs.

Browser hanya dapat membuka sejumlah koneksi TCP terbatas ke satu origin (6 untuk Chrome), sehingga meminimalkan jumlah potongan yang dihasilkan oleh pemaket dapat memastikan bahwa jumlah total permintaan tetap berada di bawah batas ini. Namun, hal ini hanya berlaku untuk HTTP/1.1. Multiplexing di HTTP/2 memungkinkan beberapa permintaan di-streaming secara paralel menggunakan satu koneksi melalui satu origin. Dengan kata lain, kita umumnya tidak perlu khawatir untuk membatasi jumlah potongan yang dikeluarkan oleh pemaket kita.

Semua browser utama mendukung HTTP/2. Tim Chrome dan Next.js ingin melihat apakah meningkatkan jumlah permintaan dengan membagi satu paket "commons" Next.js menjadi beberapa potongan bersama akan memengaruhi performa pemuatan. Mereka mulai dengan mengukur performa satu situs sambil mengubah jumlah maksimum permintaan paralel menggunakan properti maxInitialRequests.

Performa pemuatan halaman dengan peningkatan jumlah permintaan

Dalam rata-rata tiga kali menjalankan beberapa uji coba di satu halaman web, waktu load, start-render, dan First Contentful Paint semuanya tetap sama saat memvariasikan jumlah permintaan awal maksimum (dari 5 hingga 15). Menariknya, kami hanya melihat sedikit overhead performa setelah membagi secara agresif menjadi ratusan permintaan.

Performa pemuatan halaman dengan ratusan permintaan

Hal ini menunjukkan bahwa tetap berada di bawah nilai minimum yang andal (20~25 permintaan) akan menghasilkan keseimbangan yang tepat antara performa pemuatan dan efisiensi caching. Setelah beberapa pengujian dasar pengukuran, 25 dipilih sebagai jumlah maxInitialRequest.

Mengubah jumlah maksimum permintaan yang terjadi secara paralel menghasilkan lebih dari satu paket bersama, dan memisahkannya dengan tepat untuk setiap titik entri akan mengurangi jumlah kode yang tidak diperlukan secara signifikan untuk halaman yang sama.

Pengurangan payload JavaScript dengan peningkatan chunking

Eksperimen ini hanya tentang mengubah jumlah permintaan untuk melihat apakah ada efek negatif pada performa pemuatan halaman. Hasilnya menunjukkan bahwa menyetel maxInitialRequests ke 25 di halaman pengujian adalah hal yang optimal karena mengurangi ukuran payload JavaScript tanpa memperlambat halaman. Jumlah total JavaScript yang diperlukan untuk melakukan hidrasi halaman masih tetap sama, yang menjelaskan mengapa performa pemuatan halaman tidak selalu meningkat dengan pengurangan jumlah kode.

webpack menggunakan 30 KB sebagai ukuran minimum default untuk potongan yang akan dibuat. Namun, menggabungkan nilai maxInitialRequests 25 dengan ukuran minimum 20 KB justru menghasilkan caching yang lebih baik.

Pengurangan ukuran dengan potongan terperinci

Banyak framework, termasuk Next.js, mengandalkan pemilihan rute sisi klien (ditangani oleh JavaScript) untuk memasukkan tag skrip yang lebih baru untuk setiap transisi rute. Namun, bagaimana cara menentukan potongan dinamis ini terlebih dahulu pada waktu build?

Next.js menggunakan file manifes build sisi server untuk menentukan potongan output yang digunakan oleh titik entri yang berbeda. Untuk memberikan informasi ini kepada klien juga, file manifes build sisi klien yang diringkas dibuat untuk memetakan semua dependensi untuk setiap titik entri.

// Returns a promise for the dependencies for a particular route
getDependencies (route) {
  return this.promisedBuildManifest.then(
    man => (man[route] && man[route].map(url => `/_next/${url}`)) || []
  )
}
Output dari beberapa potongan bersama dalam aplikasi Next.js.

Strategi pengelompokan terperinci yang lebih baru ini pertama kali diluncurkan di Next.js di balik tanda, dan diuji pada sejumlah pengguna awal. Banyak yang mengalami pengurangan signifikan pada total JavaScript yang digunakan untuk seluruh situs mereka:

Situs Total Perubahan JS % Perbedaan
https://www.barnebys.com/ -238 KB -23%
https://sumup.com/ -220 KB -30%
https://www.hashicorp.com/ -11 MB -71%
Pengurangan ukuran JavaScript - di semua rute (dikompresi)

Versi final dikirim secara default di versi 9.2.

Gatsby

Gatsby menggunakan pendekatan yang sama dengan menggunakan heuristik berbasis penggunaan untuk menentukan modul umum:

config.optimization = {
  …
  splitChunks: {
    name: false,
    chunks: `all`,
    cacheGroups: {
      default: false,
      vendors: false,
      commons: {
        name: `commons`,
        chunks: `all`,
        // if a chunk is used more than half the components count,
        // we can assume it's pretty global
        minChunks: componentsCount > 2 ? componentsCount * 0.5 : 2,
      },
      react: {
        name: `commons`,
        chunks: `all`,
        test: /[\\/]node_modules[\\/](react|react-dom|scheduler)[\\/]/,
      },

Dengan mengoptimalkan konfigurasi webpack untuk mengadopsi strategi pengelompokan terperinci yang serupa, mereka juga melihat pengurangan JavaScript yang cukup besar di banyak situs besar:

Situs Total Perubahan JS % Perbedaan
https://www.gatsbyjs.org/ -680 KB -22%
https://www.thirdandgrove.com/ -390 KB -25%
https://ghost.org/ -1,1 MB -35%
https://reactjs.org/ -80 Kb -8%
Pengurangan ukuran JavaScript - di semua rute (dikompresi)

Lihat PR untuk memahami cara mereka menerapkan logika ini ke dalam konfigurasi webpack, yang dikirim secara default di v2.20.7.

Kesimpulan

Konsep pengiriman potongan terperinci tidak spesifik untuk Next.js, Gatsby, atau bahkan webpack. Semua orang harus mempertimbangkan untuk meningkatkan strategi pengelompokan aplikasi mereka jika mengikuti pendekatan paket "commons" yang besar, terlepas dari framework atau pemaket modul yang digunakan.

  • Jika Anda ingin melihat pengoptimalan pengelompokan yang sama diterapkan ke aplikasi React vanilla, lihat contoh aplikasi React ini. Aplikasi ini menggunakan versi strategi pengelompokan terperinci yang disederhanakan dan dapat membantu Anda mulai menerapkan jenis logika yang sama ke situs Anda.
  • Untuk Rollup, potongan dibuat secara terperinci secara default. Lihat manualChunks jika Anda ingin mengonfigurasi perilaku secara manual.