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 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 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 kerangka kerja inti pemaket. webpack v3 diperkenalkan CommonsChunkPlugin agar dapat modul {i>output<i} digunakan bersama di antara titik masuk yang berbeda dalam satu (atau beberapa) "{i>commons<i}" potongan (atau potongan). Kode yang dibagikan dapat diunduh secara terpisah dan disimpan di {i>cache browser<i} sejak dini yang dapat menghasilkan performa pemuatan yang lebih baik.

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

Konfigurasi titik entri dan paket umum

Meskipun praktis, konsep pemaketan semua kode modul bersama menjadi satu potongan memiliki keterbatasan praktik. Modul yang tidak dibagikan di setiap titik entri dapat didownload untuk rute yang tidak menggunakannya mengakibatkan lebih banyak kode yang didownload daripada yang diperlukan. Misalnya, saat page1 dimuat potongan common akan memuat kode untuk moduleC meskipun page1 tidak menggunakan moduleC. Oleh karena itu, bersama dengan beberapa yang lain, webpack v4 menghapus plugin dan menggantinya satu: SplitChunksPlugin.

Potongan yang Lebih Baik

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

Namun, banyak framework web yang menggunakan plugin ini masih mengikuti "single-common" pendekatan untuk potongan pemisahan. 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 memasukkan kode yang bergantung pada kerangka kerja ke dalam potongan bersama berarti kode tersebut dapat diunduh dan meng-cache untuk titik entri mana pun, heuristik berbasis penggunaan 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, akan ada lebih banyak kode yang tidak diperlukan yang akan didownload.
  • Jika Anda meningkatkan rasionya, lebih banyak kode yang akan diduplikasi di beberapa rute.

Untuk mengatasi masalah ini, Next.js menggunakan berbagai konfigurasi untuk SplitChunksPlugin yang mengurangi kode yang tidak diperlukan untuk rute apa pun.

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

Strategi pemotongan terperinci ini memberikan manfaat sebagai berikut:

  • Waktu muat halaman lebih cepat. Memberikan beberapa potongan bersama, bukan satu, meminimalkan jumlah kode yang tidak diperlukan (atau duplikat) untuk setiap titik masuk.
  • Meningkatkan cache selama navigasi. Memisahkan library besar dan dependensi framework menjadi potongan terpisah akan mengurangi kemungkinan invalidasi {i>cache<i} karena keduanya cenderung tidak hingga perubahan dilakukan.

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

Permintaan HTTP lainnya

SplitChunksPlugin menentukan dasar pemotongan terperinci, dan menerapkan pendekatan ini ke framework seperti Next.js bukanlah konsep yang sepenuhnya baru. Banyak {i>framework<i}, bagaimanapun, masih terus menggunakan satu heuristik dan "{i>commons<i}" karena beberapa alasan. Termasuk kekhawatiran bahwa terlalu banyak permintaan HTTP dapat berdampak negatif pada performa situs.

Browser hanya dapat membuka koneksi TCP dalam jumlah terbatas ke satu origin (6 untuk Chrome), jadi meminimalkan jumlah potongan yang dihasilkan oleh pemaket dapat memastikan bahwa jumlah total permintaan tetap berada di bawah batas ini. Namun, ini hanya berlaku untuk HTTP/1.1. Multiplexing di HTTP/2 memungkinkan beberapa permintaan di-streaming secara paralel menggunakan satu koneksi melalui satu tempat asal. Dengan kata lain, umumnya kita tidak perlu khawatir tentang 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 memisahkan satu "commons" Next.js paket menjadi beberapa bagian bersama akan mempengaruhi performa pemuatan dengan cara apa pun. Mereka memulai dengan mengukur performa 1 situs sekaligus mengubah jumlah maksimum permintaan paralel menggunakan maxInitialRequests saat ini.

Performa pemuatan halaman dengan peningkatan jumlah permintaan

Dalam rata-rata tiga kali percobaan pada satu laman web, load, start-render dan First Contentful Paint semua tetap sama saat memvariasikan nilai awal maksimum jumlah permintaan (dari 5 hingga 15). Cukup menarik, kami melihat adanya sedikit overhead kinerja setelah membagi secara agresif menjadi ratusan permintaan.

Performa pemuatan halaman dengan ratusan permintaan

Hasil ini menunjukkan bahwa tetap berada di bawah ambang batas yang dapat diandalkan (20~25 permintaan) mencapai keseimbangan yang tepat antara performa pemuatan dan efisiensi caching. Setelah beberapa pengujian dasar, 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 masuk mengurangi jumlah kode yang tidak diperlukan untuk halaman yang sama.

pengurangan payload JavaScript dengan peningkatan pemotongan

Eksperimen ini hanya bertujuan mengubah jumlah permintaan untuk melihat apakah dampak negatif terhadap performa pemuatan halaman. Hasilnya menyarankan agar menyetel maxInitialRequests ke 25 pada halaman pengujian sudah optimal karena mengurangi ukuran payload JavaScript tanpa melambat ke bagian bawah halaman. Jumlah total JavaScript yang diperlukan untuk menghidrasi halaman tetap hampir sama, yang menjelaskan mengapa kinerja pemuatan laman tidak serta merta meningkat dengan jumlah kode.

webpack menggunakan 30 KB sebagai ukuran minimum default untuk potongan yang akan dihasilkan. Namun, menggabungkan Nilai maxInitialRequests sebesar 25 dengan ukuran minimum 20 KB 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. Tetapi bagaimana mereka menentukan potongan dinamis ini terlebih dahulu pada waktu build?

Next.js menggunakan file manifes build sisi server untuk menentukan potongan output mana yang digunakan oleh titik masuk yang berbeda. Untuk memberikan informasi ini kepada klien juga, ringkasan sisi klien file manifes build 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 pemotongan terperinci yang lebih baru ini pertama kali diluncurkan di Next.js di belakang flag, tempat ia diuji pada jumlah pengguna awal. Banyak yang melihat pengurangan yang signifikan terhadap total JavaScript yang digunakan untuk situs secara keseluruhan:

Situs Perubahan JS Total % 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 dalam versi 9.2.

Gatsby

Gatsby dulunya mengikuti pendekatan yang sama, yakni menggunakan model heuristik 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 mereka untuk mengadopsi strategi pemotongan terperinci yang serupa, mereka juga melihat pengurangan JavaScript yang cukup besar di banyak situs besar:

Situs Perubahan JS Total % 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 bagaimana mereka mengimplementasikan logika ini ke dalam konfigurasi webpack mereka, yang dikirimkan secara {i>default<i} di v2.20.7.

Kesimpulan

Konsep pengiriman potongan terperinci tidak spesifik untuk Next.js, Gatsby, atau bahkan webpack. Siapa saja sebaiknya mempertimbangkan untuk meningkatkan strategi pengelompokan aplikasi jika mengikuti “keadaan umum” yang besar apa pun, apa pun framework atau pemaket modul yang digunakan.

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