Ayrıntılı ögelemeyle, Next.js ve Gatsby sayfa yükleme performansı iyileştirildi

Next.js ve Gatsby'deki daha yeni bir Webpack parçalara ayırma stratejisi, sayfa yükleme performansını artırmak için yinelenen kodu en aza indirir.

Chrome, JavaScript açık kaynak ekosistemindeki araç ve çerçevelerle ortak çalışıyor. Next.js ve Gatsby'nin yükleme performansını artırmak için kısa süre önce bir dizi yeni optimizasyon eklendi. Bu makalede artık her iki çerçevede de varsayılan olarak kullanıma sunulan iyileştirilmiş ayrıntılı parçalama stratejisi ele alınmaktadır.

Birçok web çerçevesi gibi Next.js ve Gatsby de temel paketleyici olarak webpack'ı kullanır. webpack v3, farklı giriş noktaları arasında paylaşılan modülleri tek bir (veya birkaç) "ortak" parçada (veya parçalarda) yayınlamayı mümkün kılmak için CommonsChunkPlugin'i kullanıma sundu. Paylaşılan kod ayrı olarak indirilebilir ve tarayıcı önbelleğine erkenden depolanabilir. Bu da daha iyi bir yükleme performansı sağlayabilir.

Bu kalıp, aşağıdaki gibi bir giriş noktası ve paket yapılandırmasını benimseyen birçok tek sayfalık uygulama çerçevesiyle popüler hale geldi:

Ortak giriş noktası ve paket yapılandırması

Tüm paylaşılan modül kodlarını tek bir parçaya toplama kavramı pratik olsa da sınırlamaları vardır. Her giriş noktasında paylaşılmayan modüller, kullanmayan rotalar için indirilebilir. Bu durumda, gereğinden fazla kod indirilir. Örneğin, page1 common parçasını yüklediğinde, page1 moduleC kullanmasa bile moduleC kodunu yükler. Bu nedenle, webpack v4, birkaç ek eklentiyle birlikte bu eklentiyi kaldırıp yerine yeni bir eklenti ekledi: SplitChunksPlugin.

İyileştirilmiş Chunking

SplitChunksPlugin için varsayılan ayarlar çoğu kullanıcı için uygundur. Birden fazla rotada yinelenen kodun getirilmesini önlemek için çeşitli koşullara bağlı olarak birden fazla bölünmüş parça oluşturulur.

Bununla birlikte, bu eklentiyi kullanan birçok web çerçevesi hâlâ parça bölme için "tek ortak" yaklaşımını izlemektedir. Örneğin Next.js, sayfaların %50'sinden fazlasında kullanılan tüm modülleri ve tüm çerçeve bağımlılıkları (react, react-dom vb.) içeren bir commons paketi oluşturur.

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)[\\/]/,
      },
    },
  },

Paylaşılan bir parçaya çerçeveye bağımlı kod eklemek, bu kodun herhangi bir giriş noktası için indirilip önbelleğe alınabileceği anlamına gelse de sayfaların yarısından fazlasında kullanılan ortak modülleri eklemeyle ilgili kullanıma dayalı sezgisel yaklaşım çok etkili değildir. Bu oran değiştirildiğinde yalnızca iki sonuçtan biri elde edilir:

  • Oranı düşürürseniz daha fazla gereksiz kod indirilir.
  • Oran arttıkça birden fazla rotada daha fazla kod kopyalanır.

Next.js, bu sorunu çözmek amacıyla SplitChunksPlugin için herhangi bir rota için gereksiz kodları azaltan farklı bir yapılandırma kullandı.

  • Yeterince büyük üçüncü taraf modülleri (160 KB'tan büyük) kendi ayrı parçalarına ayrılır
  • Çerçeve bağımlılıkları (react, react-dom vb.) için ayrı bir frameworks yığını oluşturulur
  • Gerektiği kadar paylaşılan parça oluşturulur (en fazla 25)
  • Yığının oluşturulması için minimum boyut 20 KB olarak değiştirildi

Bu ayrıntılı parçalara ayırma stratejisi aşağıdaki avantajları sağlar:

  • Sayfa yüklenme süreleri iyileştirildi. Tek bir paylaşılamaz blok yerine birden fazla paylaşılamaz blok yayınlamak, herhangi bir giriş noktası için gereksiz (veya yinelenen) kod miktarını en aza indirir.
  • Gezinme sırasında iyileştirilmiş önbelleğe alma. Büyük kitaplıkları ve çerçeve bağımlılıkları ayrı parçalara bölmek, yükseltme yapılana kadar ikisinin de değişme olasılığı düşük olduğundan önbelleğin geçersiz kılınmasını azaltır.

Next.js'in benimsediği yapılandırmaların tamamını webpack-config.ts adresinde görebilirsiniz.

Daha fazla HTTP isteği

SplitChunksPlugin, ayrıntılı parçalara ayırmanın temelini tanımladı ve bu yaklaşımı Next.js gibi bir çerçeveye uygulamak tamamen yeni bir kavram değildi. Ancak birçok çerçeve, birkaç nedenden dolayı tek bir sezgisel ve "ortak" paket stratejisi kullanmaya devam etti. Bu durum, çok daha fazla HTTP isteğinin site performansını olumsuz yönde etkileyebileceği endişesini de içerir.

Tarayıcılar tek bir kaynakla yalnızca sınırlı sayıda TCP bağlantısı açabilir (Chrome için 6). Bu nedenle, bir birleştirici tarafından oluşturulan parça sayısını en aza indirmek, toplam istek sayısının bu eşiğin altında kalmasını sağlayabilir. Ancak bu durum yalnızca HTTP/1.1 için geçerlidir. HTTP/2'de Multiplex, tek bir kaynak üzerinden tek bir bağlantı kullanılarak birden fazla isteğin paralel olarak akışına olanak tanır. Diğer bir deyişle, genellikle paketleyicimiz tarafından yayınlanan parça sayısını sınırlama konusunda endişelenmemize gerek yoktur.

Başlıca tarayıcıların tümü HTTP/2'yi destekler. Chrome ve Next.js ekipleri, Next.js'in tek "commons" paketini birden fazla paylaşılan parçaya ayırarak istek sayısını artırmanın yükleme performansını herhangi bir şekilde etkileyip etkilemediğini görmek istedi. maxInitialRequests mülkünü kullanarak maksimum paralel istek sayısını değiştirirken tek bir sitenin performansını ölçerek başladılar.

Artan istek sayısıyla sayfa yükleme performansı

Tek bir web sayfasında ortalama üç kez yapılan birden fazla denemede, maksimum ilk istek sayısı (5 ila 15 arasında) değiştiğinde load, render'ı başlatma ve İlk Zengin İçerikli Boyama sürelerinin tümü yaklaşık olarak aynı kalmıştır. İlginç bir şekilde, yalnızca yüzlerce isteğe agresif bir şekilde bölündükten sonra küçük bir performans yükü fark ettik.

Yüzlerce istek içeren sayfa yükleme performansı

Bu sonuçlar, güvenilir bir eşiğin (20-25 istek) altında kalmanın, yükleme performansı ile önbelleğe alma verimliliği arasında doğru dengeyi sağladığını gösterdi. Bazı temel testlerden sonra maxInitialRequest sayısı olarak 25 seçildi.

Paralel olarak gerçekleşen maksimum istek sayısını değiştirmek, tek bir paylaşılan paketten daha fazlasına yol açtı ve bunları her giriş noktası için uygun şekilde ayırmak, aynı sayfa için gereksiz kod miktarını önemli ölçüde azalttı.

Daha fazla parçalama ile JavaScript yükünü azaltır

Bu deneme, yalnızca sayfa yükleme performansında olumsuz bir etki olup olmayacağını görmek için istek sayısını değiştirmekle ilgiliydi. Sonuçlar, sayfayı yavaşlatmadan JavaScript yükü boyutunu azalttığı için test sayfasında maxInitialRequests değerinin 25 olarak ayarlanmasının en uygun seçenek olduğunu gösteriyor. Sayfayı beslemek için gereken toplam JavaScript miktarı yaklaşık olarak aynı kalmıştır. Bu da sayfa yükleme performansının, kod miktarının azaltılmasıyla mutlaka iyileşmediğini açıklar.

webpack, oluşturulacak bir parça için varsayılan minimum boyut olarak 30 KB'yı kullanır. Ancak 25 değerine sahip bir maxInitialRequests değerini 20 KB minimum boyutla birlikte kullanmak daha iyi önbelleğe almayla sonuçlandı.

Ayrıntılı parçalarla boyut azaltma

Next.js dahil olmak üzere birçok çerçeve, her rota geçişi için yeni komut dosyası etiketleri eklemek üzere istemci tarafı yönlendirmeyi (JavaScript tarafından yönetilir) kullanır. Peki bu dinamik parçaları derleme sırasında nasıl önceden belirleyebilirler?

Next.js, farklı giriş noktaları tarafından hangi çıkış parçalarının kullanıldığını belirlemek için sunucu tarafı bir derleme manifest dosyası kullanır. Bu bilgileri istemciye de sağlamak için her giriş noktasının tüm bağımlılıkları eşlemek üzere kısaltılmış bir istemci tarafı derleme manifest dosyası oluşturuldu.

// 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}`)) || []
  )
}
Bir Next.js uygulamasında birden fazla paylaşılan parçanın çıkışı.

Bu yeni ayrıntılı parçalara ayırma stratejisi, ilk olarak Next.js'de bir işaretle kullanıma sunuldu ve burada bir dizi ilk kullanıcı üzerinde test edildi. Birçok kullanıcı, sitelerinin tamamı için kullanılan toplam JavaScript miktarında önemli düşüşler elde etti:

Web sitesi Toplam JS Değişikliği % Farkı
https://www.barnebys.com/ -238 KB -%23
https://sumup.com/ -220 KB -%30
https://www.hashicorp.com/ -11 MB -%71
JavaScript boyutu azaltma - tüm rotalarda (sıkıştırılmış)

Nihai sürüm varsayılan olarak 9.2 sürümünde yayınlandı.

Gatsby

Gatsby, ortak modülleri tanımlamak için kullanıma dayalı bulgusal bir yöntemden yararlanırken aynı yaklaşımı benimsemiştir:

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)[\\/]/,
      },

Benzer bir ayrıntılı parçalara ayırma stratejisi benimsemek için webpack yapılandırmalarını optimize ederek birçok büyük sitede JavaScript'te önemli ölçüde düşüşler de fark ettiler:

Web sitesi Toplam JS Değişikliği % Farkı
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
JavaScript boyutu azaltma - tüm rotalarda (sıkıştırılmış)

Bu mantığı, varsayılan olarak v2.20.7 sürümünde sunulan webpack yapılandırmalarına nasıl uyguladıklarını öğrenmek için PR'ye göz atın.

Sonuç

Ayrıntılı parçaları gönderme kavramı Next.js, Gatsby'ye, hatta webpack'e özgü değildir. Kullanılan çerçeve veya modül paketleyiciden bağımsız olarak, büyük bir "ortak" paket yaklaşımı izliyorsa herkes uygulamalarının parçalara ayırma stratejisini iyileştirmeyi düşünmelidir.

  • Aynı parçalara ayırma optimizasyonlarının, basit bir React uygulamasına uygulandığını görmek istiyorsanız bu örnek React uygulamasına göz atın. Ayrıntılı parçalara ayırma stratejisinin basitleştirilmiş bir sürümünü kullanan bu uygulama, sitenize aynı tür mantığı uygulamaya başlamanıza yardımcı olabilir.
  • Toplama için parçalar varsayılan olarak ayrıntılı şekilde oluşturulur. Davranışı manuel olarak yapılandırmak istiyorsanız manualChunks bölümüne göz atın.