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

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

Chrome berkolaborasi dengan alat dan framework di 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 pemotongan terperinci yang lebih baik, yang kini dikirimkan secara default di kedua framework.

Pengantar

Seperti banyak framework web lainnya, Next.js dan Gatsby menggunakan webpack sebagai pemaket inti. webpack v3 memperkenalkan CommonsChunkPlugin agar modul output yang dapat dibagikan di antara titik entri yang berbeda-beda dalam satu (atau beberapa) bagian "umum" (atau bagian). Kode yang dibagikan dapat didownload secara terpisah dan disimpan di cache browser lebih awal, sehingga dapat menghasilkan performa pemuatan yang lebih baik.

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

Konfigurasi entrypoint dan paket umum

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

Peningkatan Chunking

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

Namun, banyak framework web yang menggunakan plugin ini masih mengikuti pendekatan "single-commons" untuk pemisahan potongan. 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 seterusnya).

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 setiap titik entri, heuristik berbasis penggunaan untuk menyertakan modul umum yang digunakan di lebih dari setengah halaman tidaklah terlalu efektif. Memodifikasi rasio ini hanya akan memberikan salah satu dari dua hasil:

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

Untuk mengatasi masalah ini, Next.js mengadopsi konfigurasi berbeda untuk SplitChunksPlugin yang mengurangi kode yang tidak diperlukan untuk semua rute.

  • Setiap modul pihak ketiga yang berukuran cukup besar (lebih dari 160 KB) akan dibagi menjadi bagian masing-masingnya
  • Potongan frameworks terpisah dibuat untuk dependensi framework (react, react-dom, dan seterusnya)
  • Potongan bersama sebanyak yang diperlukan akan dibuat (hingga 25)
  • Ukuran minimum untuk potongan yang akan dibuat diubah menjadi 20 KB

Strategi pemotongan terperinci ini memberikan manfaat sebagai berikut:

  • Waktu muat halaman disempurnakan. Dengan mengirim beberapa bagian bersama, bukan hanya satu, akan meminimalkan jumlah kode yang tidak diperlukan (atau duplikat) untuk setiap titik entri.
  • Peningkatan cache selama navigasi. Memisahkan library besar dan dependensi framework menjadi bagian-bagian terpisah akan mengurangi kemungkinan pembatalan cache karena keduanya tidak mungkin berubah hingga upgrade dilakukan.

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

Permintaan HTTP lainnya

SplitChunksPlugin menentukan dasar untuk pemotongan terperinci, dan menerapkan pendekatan ini ke framework seperti Next.js bukanlah konsep yang sepenuhnya baru. Namun, banyak framework masih menggunakan satu strategi paket heuristik dan "umum" karena beberapa alasan. Hal ini mencakup kekhawatiran bahwa semakin banyak permintaan HTTP dapat berpengaruh negatif terhadap performa situs.

Browser hanya dapat membuka koneksi TCP dalam jumlah 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, secara umum kita tidak perlu khawatir akan membatasi jumlah potongan yang dimunculkan oleh pemaket.

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

Performa pemuatan halaman dengan peningkatan jumlah permintaan

Rata-rata tiga kali percobaan dalam satu halaman web, waktu load, mulai-render, dan First Contentful Paint semuanya tetap sama saat memvariasikan jumlah permintaan awal maksimum (dari 5 hingga 15). Cukup menarik, kami melihat sedikit overhead performa hanya setelah membagi secara agresif ke ratusan permintaan.

Performa pemuatan halaman dengan ratusan permintaan

Hal ini menunjukkan bahwa tetap di bawah batas yang dapat diandalkan (20~25 permintaan) mencapai keseimbangan yang tepat antara performa pemuatan dan efisiensi penyimpanan dalam cache. Setelah beberapa pengujian dasar pengukuran, 25 dipilih sebagai jumlah maxInitialRequest.

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

Pengurangan payload JavaScript dengan peningkatan pemotongan

Eksperimen ini hanya bertujuan untuk mengubah jumlah permintaan guna melihat apakah akan ada efek negatif terhadap performa pemuatan halaman. Hasilnya menunjukkan bahwa penetapan maxInitialRequests ke 25 di halaman pengujian sudah optimal karena dapat mengurangi ukuran payload JavaScript tanpa memperlambat halaman. Jumlah total JavaScript yang diperlukan untuk menghidrasi halaman masih kurang sama, yang menjelaskan mengapa performa pemuatan halaman tidak selalu meningkat seiring dengan pengurangan jumlah kode.

webpack menggunakan 30 KB sebagai ukuran minimum {i>default<i} untuk pembuatan potongan data. Namun, menggabungkan nilai maxInitialRequests sebesar 25 dengan ukuran minimum 20 KB akan menghasilkan caching yang lebih baik.

Pengurangan ukuran dengan potongan terperinci

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

Next.js menggunakan file manifes build sisi server untuk menentukan potongan yang di-output yang digunakan oleh titik entri yang berbeda. Untuk memberikan informasi ini kepada klien juga, file manifes build sisi klien ringkas telah dibuat untuk memetakan semua dependensi bagi 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 bagian bersama dalam aplikasi Next.js.

Strategi pemotongan terperinci yang lebih baru ini pertama kali diluncurkan di Next.js di belakang tanda, yang diuji pada sejumlah pengguna awal. Banyak dari mereka melihat penurunan yang 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 dikirimkan secara default di versi 9.2.

Gatsby

Gatsby dulu mengikuti pendekatan yang sama dalam 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 pemotongan 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 mereka, yang dikirimkan secara default di v2.20.7.

Kesimpulan

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

  • Jika Anda ingin melihat pengoptimalan potongan yang sama yang diterapkan pada aplikasi React biasa, lihat contoh aplikasi React ini. Contoh ini menggunakan versi sederhana strategi pemotongan terperinci dan dapat membantu Anda mulai menerapkan logika yang sama ke situs.
  • Untuk Penggabungan, bagian dibuat secara terperinci secara default. Lihat manualChunks jika Anda ingin mengonfigurasi perilaku secara manual.