Akışları olan daha hızlı çok sayfalı uygulamalar

Bugünlerde, web siteleri (veya tercih ederseniz web uygulamaları) iki gezinme planından birini kullanma eğilimindedir:

  • Gezinme şeması tarayıcıları varsayılan olarak sağlar. Yani tarayıcınızın adres çubuğuna bir URL girdiğinizde, gezinme isteği yanıt olarak bir doküman döndürür. Ardından, geçerli dokümanı başka bir doküman olan ad infinitum için kaldıran bir bağlantıyı tıklarsınız.
  • Uygulama kabuğunu yüklemek üzere ilk gezinme isteğini içeren ve her "gezinme" için bir arka uç API'sinin içeriğiyle uygulama kabuğunu istemci tarafından oluşturulan işaretlemeyle doldurmak için JavaScript'ten yararlanan tek sayfalık uygulama kalıbı.

Her yaklaşımın faydaları, taraftarları tarafından dile getiriliyor:

  • Rotaların erişilebilir olması JavaScript gerektirmediğinden, tarayıcıların varsayılan olarak sağladığı gezinme şeması esnektir. İşaretlemenin JavaScript aracılığıyla istemci tarafından oluşturulması da potansiyel olarak pahalı bir işlem olabilir. Bu da, alt uç cihazların, içerik sağlayan komut dosyalarını işlemesinin engellenmesi nedeniyle içeriğin gecikmesine neden olabilir.
  • Diğer yandan, Tek Sayfalık Uygulamalar (SPA'lar) ilk yüklemeden sonra daha hızlı gezinme olanağı sağlayabilir. Tamamen yeni bir doküman için tarayıcının bir dokümanın yüklemesini kaldırmak (ve bunu her gezinmede tekrarlamak) yerine, JavaScript'in çalışması gerekse bile daha hızlı, daha "uygulama benzeri" bir deneyim sunabilirler.

Bu yayında, yukarıda açıklanan iki yaklaşım arasında denge kuran üçüncü bir yöntemden bahsedeceğiz: üstbilgi ve altbilgi işaretleme gibi ortak web öğelerini önbelleğe almak için bir hizmet çalışanı kullanma ve tarayıcının varsayılan gezinme şemasını kullanmaya devam ederken müşteriye mümkün olduğunca hızlı bir HTML yanıtı sunmak için akışları kullanma.

Neden hizmet çalışanında HTML yanıtları akışı sağlamalısınız?

Canlı yayın, web tarayıcınızın istekte bulunduğunda zaten yaptığı bir şeydir. Bu, gezinme istekleri bağlamında son derece önemlidir. Çünkü tarayıcının, doküman işaretlemesini ayrıştırmaya ve sayfa oluşturmaya başlamadan önce yanıtın tamamını beklemesini engellememesini sağlar.

Akış olmayan HTML ile akışlı HTML'yi gösteren şema. İlk durumda, işaretleme yükünün tamamı gelene kadar işlenmez. İkincisinde, işaretleme ağdan parçalar halinde geldikçe, işaretleme artımlı olarak işlenir.

Hizmet çalışanları için akış, JavaScript Streams API'yi kullandığından biraz farklıdır. Hizmet çalışanının yerine getirdiği en önemli görev, gezinme istekleri de dahil olmak üzere isteklere müdahale etmek ve bunları yanıtlamaktır.

Bu istekler önbellekle çeşitli şekillerde etkileşime girebilir ancak işaretleme için yaygın olarak kullanılan bir önbelleğe alma kalıbı, öncesinde alınan bir yanıtın kullanılmasını tercih etmek, ancak daha eski bir kopya varsa önbelleği kullanmak ve kullanılabilir bir yanıt önbellekte yoksa isteğe bağlı olarak genel bir yedek yanıtı sağlamaktır.

Bu, iyi çalışan işaretleme için zamana bağlı bir kalıptır, ancak çevrimdışı erişim açısından güvenilirlik konusunda yardımcı olsa da, öncelikli olarak ağ stratejisini veya yalnızca ağ stratejisini temel alan gezinme istekleri için doğal olarak herhangi bir performans avantajı sunmaz. Akış burada devreye girer. Çok sayfalı web sitenizde gezinme isteklerini hızlandırmak için Workbox hizmet çalışanınızda Streams API destekli workbox-streams modülünü nasıl kullanacağınızı keşfedeceğiz.

Tipik bir web sayfasının dökümünü alma

Yapısal açıdan konuşmak gerekirse, web siteleri her sayfada ortak öğeler bulunma eğilimindedir. Sayfa öğelerinin tipik bir düzenlemesi genellikle şuna benzer:

  • Başlık.
  • İçerik.
  • Altbilgi.

Örnek olarak web.dev kullanılırsa ortak öğelerin dökümü aşağıdaki gibi olur:

web.dev web sitesindeki ortak öğelerin dökümü. Betimlenen ortak alanlar "başlık", "içerik" ve "altbilgi"dir.

Bir sayfanın bölümlerini tanımlamanın amacı, ağa gitmeden nelerin önceden önbelleğe alınıp alınabileceğini (yani tüm sayfalarda ortak olan üstbilgi ve altbilgi işaretlemeyi) ve bu durumda, sayfanın her zaman ilk olarak ağa gideceğimiz bölümünü belirlemektir.

Bir sayfanın bölümlerini nasıl segmentlere ayıracağımızı ve ortak öğeleri nasıl tanımlayacağınızı öğrendiğimizde, yalnızca ağdan içerik isterken üstbilgi ve altbilgi işaretlemesini her zaman önbellekten anında alan bir hizmet çalışanı yazabiliriz.

Ardından workbox-streams üzerinden Streams API'sini kullanarak tüm bu parçaları birleştirip gezinme isteklerine anında yanıt verebiliriz. Bunu yaparken de ağdan gereken minimum işaretleme miktarını talep edebiliriz.

Akış hizmeti çalışanı oluşturma

Service Worker'da kısmi içerik akışı çok önemlidir. Ancak web sitenizi nasıl yapılandıracağınızdan başlayarak sürecin her adımı ilerledikçe ayrıntılı olarak keşfedilecek.

Web sitenizi kısmi bölümlere ayırma

Bir akış hizmeti çalışanı yazmaya başlamadan önce üç şey yapmanız gerekir:

  1. Yalnızca web sitenizin başlık işaretlemesini içeren bir dosya oluşturun.
  2. Yalnızca web sitenizin altbilgi işaretlemesini içeren bir dosya oluşturun.
  3. Her sayfanın ana içeriğini ayrı bir dosyaya çıkarın veya arka ucunuzu, yalnızca bir HTTP istek başlığına göre yalnızca sayfa içeriğini koşullu olarak sunacak şekilde ayarlayın.

Tahmin edebileceğiniz gibi, özellikle web siteniz statikse son adım en zor adımdır. Bu durum sizin için geçerliyse her sayfanın iki sürümünü oluşturmanız gerekir: Bir sürüm tam sayfa işaretlemesini, diğeri ise yalnızca içeriği içerir.

Akış hizmeti çalışanı oluşturma

workbox-streams modülünü yüklemediyseniz bu işlemi, yüklü olan Workbox modüllerine ek olarak yapmanız gerekir. Bu örnekte, bu işlem aşağıdaki paketleri içerir:

npm i workbox-navigation-preload workbox-strategies workbox-routing workbox-precaching workbox-streams --save

Bu aşamadan sonraki adım, yeni hizmet çalışanınızı oluşturup üstbilgi ve altbilgi bölümlerinizi önbelleğe almaktır.

Kısmi önbelleğe alma

İlk olarak sw.js adlı projenizin (veya tercih ettiğiniz dosya adının) kök dizininde bir hizmet çalışanı oluşturun. Bu eğitimde, aşağıdaki bilgilerle başlayacaksınız:

// sw.js
import * as navigationPreload from 'workbox-navigation-preload';
import {NetworkFirst} from 'workbox-strategies';
import {registerRoute} from 'workbox-routing';
import {matchPrecache, precacheAndRoute} from 'workbox-precaching';
import {strategy as composeStrategies} from 'workbox-streams';

// Enable navigation preload for supporting browsers
navigationPreload.enable();

// Precache partials and some static assets
// using the InjectManifest method.
precacheAndRoute([
  // The header partial:
  {
    url: '/partial-header.php',
    revision: __PARTIAL_HEADER_HASH__
  },
  // The footer partial:
  {
    url: '/partial-footer.php',
    revision: __PARTIAL_FOOTER_HASH__
  },
  // The offline fallback:
  {
    url: '/offline.php',
    revision: __OFFLINE_FALLBACK_HASH__
  },
  ...self.__WB_MANIFEST
]);

// To be continued...

Bu kodun birkaç işlevi vardır:

  1. Destekleyen tarayıcılar için gezinme önceden yüklemesini etkinleştirir.
  2. Üstbilgi ve altbilgi işaretlemesini önbelleğe alır. Bu, ağ tarafından engellenmeyeceğinden, her sayfanın üstbilgi ve altbilgi işaretlemesinin anında alınacağı anlamına gelir.
  3. injectManifest yöntemini kullanan __WB_MANIFEST yer tutucusundaki statik öğeleri önbelleğe alır.

Yanıt akışı

Hizmet çalışanınızın birleştirilmiş yanıt akışı elde etmesini sağlamak tüm bu çabanın en önemli parçasıdır. Yine de Workbox ve workbox-streams, tüm bunları kendi başınıza yapmayı gerektiren duruma kıyasla bu süreci çok daha kısa ve öz hale getiriyor:

// sw.js
import * as navigationPreload from 'workbox-navigation-preload';
import {NetworkFirst} from 'workbox-strategies';
import {registerRoute} from 'workbox-routing';
import {matchPrecache, precacheAndRoute} from 'workbox-precaching';
import {strategy as composeStrategies} from 'workbox-streams';

// ...
// Prior navigation preload and precaching code omitted...
// ...

// The strategy for retrieving content partials from the network:
const contentStrategy = new NetworkFirst({
  cacheName: 'content',
  plugins: [
    {
      // NOTE: This callback will never be run if navigation
      // preload is not supported, because the navigation
      // request is dispatched while the service worker is
      // booting up. This callback will only run if navigation
      // preload is _not_ supported.
      requestWillFetch: ({request}) => {
        const headers = new Headers();

        // If the browser doesn't support navigation preload, we need to
        // send a custom `X-Content-Mode` header for the back end to use
        // instead of the `Service-Worker-Navigation-Preload` header.
        headers.append('X-Content-Mode', 'partial');

        // Send the request with the new headers.
        // Note: if you're using a static site generator to generate
        // both full pages and content partials rather than a back end
        // (as this example assumes), you'll need to point to a new URL.
        return new Request(request.url, {
          method: 'GET',
          headers
        });
      },
      // What to do if the request fails.
      handlerDidError: async ({request}) => {
        return await matchPrecache('/offline.php');
      }
    }
  ]
});

// Concatenates precached partials with the content partial
// obtained from the network (or its fallback response).
const navigationHandler = composeStrategies([
  // Get the precached header markup.
  () => matchPrecache('/partial-header.php'),
  // Get the content partial from the network.
  ({event}) => contentStrategy.handle(event),
  // Get the precached footer markup.
  () => matchPrecache('/partial-footer.php')
]);

// Register the streaming route for all navigation requests.
registerRoute(({request}) => request.mode === 'navigate', navigationHandler);

// Your service worker can end here, or you can add more
// logic to suit your needs, such as runtime caching, etc.

Bu kod, aşağıdaki gereksinimleri karşılayan üç ana bölümden oluşur:

  1. İçerik kısmi isteklerini işlemek için NetworkFirst stratejisi kullanılır. Bu strateji kullanılarak, content özel önbellek adı içerik kısmilerini ve gezinme ön yüklemesini desteklemeyen (ve bu nedenle Service-Worker-Navigation-Preload üstbilgisi gönderen) tarayıcılar için X-Content-Mode istek başlığının ayarlanıp ayarlanmayacağını işleyen özel bir eklenti içerecek şekilde belirtilir. Bu eklenti, bir içeriğin önbelleğe alınmış son sürümünün kısmi olarak mı gönderileceğini, yoksa geçerli istek için önbelleğe alınmış hiçbir sürümün depolanmaması durumunda çevrimdışı bir yedek sayfa mı gönderileceğini de belirler.
  2. workbox-streams içindeki strategy yöntemi (burada composeStrategies olarak diğer ad verilmiştir), önbelleğe alınmış üst bilgi ve alt bilgi bölümlerini ağdan kısmi olarak istenen içerikle birleştirmek için kullanılır.
  3. Tüm şema, gezinme istekleri için registerRoute ile donatılmıştır.

Bu mantıkla hareket ederek yanıt akış şeklinde ayarlayabiliriz. Bununla birlikte, ağdan gelen içeriğin önceden önbelleğe alınmış kısmi sayfalarla birleştirebileceğiniz kısmi bir sayfa olmasını sağlamak için arka uçta yapmanız gereken bazı işlemler olabilir.

Web sitenizin bir arka ucu varsa

Gezinme önceden yükleme özelliği etkinleştirildiğinde, tarayıcının true değerine sahip bir Service-Worker-Navigation-Preload üstbilgisi gönderdiğini hatırlayacaksınız. Ancak yukarıdaki kod örneğinde, etkinlik gezinme ön yüklemesi tarayıcıda desteklenmeyen özel bir X-Content-Mode üst bilgisi gönderdik. Arka uçta, yanıtı bu üstbilgilerin varlığına göre değiştirirsiniz. Bir PHP arka ucunda bu, belirli bir sayfa için aşağıdaki gibi görünebilir:

<?php
// Check if we need to render a content partial
$navPreloadSupported = isset($_SERVER['HTTP_SERVICE_WORKER_NAVIGATION_PRELOAD']) && $_SERVER['HTTP_SERVICE_WORKER_NAVIGATION_PRELOAD'] === 'true';
$partialContentMode = isset($_SERVER['HTTP_X_CONTENT_MODE']) && $_SERVER['HTTP_X_CONTENT_MODE'] === 'partial';
$isPartial = $navPreloadSupported || $partialContentMode;

// Figure out whether to render the header
if ($isPartial === false) {
  // Get the header include
  require_once($_SERVER['DOCUMENT_ROOT'] . '/includes/site-header.php');

  // Render the header
  siteHeader();
}

// Get the content include
require_once('./content.php');

// Render the content
content($isPartial);

// Figure out whether to render the footer
if ($isPartial === false) {
  // Get the footer include
  require_once($_SERVER['DOCUMENT_ROOT'] . '/includes/site-footer.php');

  // Render the footer
  siteFooter();
}
?>

Yukarıdaki örnekte içerik kısmileri işlev olarak çağrılır. Bunlar, kısmilerin oluşturulma şeklini değiştirmek için $isPartial değerini alır. Örneğin, content oluşturucu işlevi, kısmi olarak alındığında yalnızca belirli koşullardaki işaretlemeleri içerebilir. Bu işaretleme, kısa süre içinde kapsanacaktır.

Dikkat edilmesi gereken noktalar

Akış ve parçaları birleştirmesi için bir hizmet çalışanı dağıtmadan önce göz önünde bulundurmanız gereken bazı noktalar vardır. Bir Service Worker'ın bu şekilde kullanılması tarayıcının varsayılan gezinme davranışını temelden değiştirmemekle birlikte, muhtemelen düzeltmeniz gereken bazı konular vardır.

Gezinme sırasında sayfa öğelerini güncelleme

Bu yaklaşımın en zor kısmı, istemcide bazı şeylerin güncellenmesinin gerekmesidir. Örneğin, başlık işaretlemesini önbelleğe almak, sayfanın <title> öğesinde aynı içeriğe sahip olacağı anlamına gelir. Hatta gezinme öğelerinin açık/kapalı durumlarını yönetmenin bile her gezinmede güncellenmesi gerekir. Her gezinme isteği için istemcide bu ve diğer öğelerin güncellenmesi gerekebilir.

Bu sorunu aşmanın yolu, birkaç önemli şeyi güncellemek için ağdan gelen içerik parçasına satır içi <script> öğesi yerleştirmek olabilir:

<!-- The JSON below contains information about the current page. -->
<script id="page-data" type="application/json">'{"title":"Sand Wasp &mdash; World of Wasps","description":"Read all about the sand wasp in this tidy little post."}'</script>
<script>
  const pageData = JSON.parse(document.getElementById('page-data').textContent);

  // Update the page title
  document.title = pageData.title;
</script>
<article>
  <!-- Page content omitted... -->
</article>

Bu, Service Worker'ı kullanmaya karar vermeniz durumunda yapmanız gerekenlerin yalnızca bir örneğidir. Örneğin, kullanıcı bilgileri içeren daha karmaşık uygulamalar için alakalı verilerin bir kısmını localStorage gibi bir web mağazasında depolamanız ve sayfayı buradan güncellemeniz gerekebilir.

Yavaş ağlarla başa çıkma

Ön önbellekten işaretleme kullanan akış yanıtlarının bir dezavantajı, ağ bağlantıları yavaş olduğunda ortaya çıkabilir. Sorun, ön önbellekten gelen başlık işaretlemesinin anında gelmesidir, ancak başlık işaretlemesinin ilk boyanmasından sonra ağdan kısmi içeriğin ulaşması uzun sürebilir.

Bu durum kafa karıştırıcı bir deneyim yaratabilir ve ağlar çok yavaşsa, sayfa bozukmuş gibi bile gelebilir ve daha fazla görüntülenemiyor olabilir. Bu gibi durumlarda, içerik kısmi işaretlemesine, içerik yüklendikten sonra gizleyebileceğiniz bir yükleme simgesi veya ileti yerleştirmeyi seçebilirsiniz.

Bunu yapmanın bir yolu CSS'dir. Başlığınızın bir bölümünün, içeriğin bir kısmı doldurulmak üzere gelene kadar boş olan bir açılış <article> öğesiyle bittiğini varsayalım. Şuna benzer bir CSS kuralı yazabilirsiniz:

article:empty::before {
  text-align: center;
  content: 'Loading...';
}

Bu işe yarar ancak ağ hızından bağımsız olarak istemcide bir yükleme mesajı gösterilir. Tuhaf şekilde yanıp sönen mesajlarla karşılaşmamak istiyorsanız seçiciyi yukarıdaki snippet'e bir slow sınıfının içine yerleştirdiğimiz şu yaklaşımı deneyebilirsiniz:

.slow article:empty::before {
  text-align: center;
  content: 'Loading...';
}

Buradan, belirli bağlantı türlerinde <html> öğesine slow sınıfını eklemek için etkili bağlantı türünü (en azından Chromium tarayıcılarında) okumak için başlığınızda kısmi JavaScript'i kullanabilirsiniz:

<script>
  const effectiveType = navigator?.connection?.effectiveType;

  if (effectiveType !== '4g') {
    document.documentElement.classList.add('slow');
  }
</script>

Bu işlem, 4g türünden daha yavaş olan etkili bağlantı türlerinin yükleme mesajı almasını sağlar. Daha sonra içerik bölümünde, yükleme mesajından kurtulmak için slow sınıfını HTML'den kaldırmak üzere satır içi <script> öğesi yerleştirebilirsiniz:

<script>
  document.documentElement.classList.remove('slow');
</script>

Yedek yanıt sağlama

İçerik kısmileriniz için ağ öncelikli bir strateji kullandığınızı varsayalım. Kullanıcı çevrimdışıysa ve daha önce ziyaret ettiği bir sayfaya giderse kullanıcının içeriği kapatılmıştır. Ancak, henüz gitmedikleri bir sayfaya giderlerse hiçbir şey almazlar. Bunu önlemek için yedek bir yanıt sunmanız gerekir.

Yedek yanıtı elde etmek için gereken kod, önceki kod örneklerinde gösterilmiştir. Bu işlem iki adım gerektirir:

  1. Çevrimdışı yedek yanıtı önbelleğe alın.
  2. Ağ öncelikli stratejiniz için eklentide handlerDidError geri çağırma özelliğini ayarlayarak sayfanın son erişilen sürümünün önbelleğini kontrol edin. Sayfaya hiç erişilmediyse ön önbellekten yedek yanıtı almak için workbox-precaching modülündeki matchPrecache yöntemini kullanmanız gerekir.

Önbelleğe alma ve CDN'ler

Service Workerınızda bu akış kalıbını kullanıyorsanız aşağıdaki durumların sizin için geçerli olup olmadığını değerlendirin:

  • CDN veya başka bir tür ara/genel önbellek kullanıyorsunuz.
  • public yönergesiyle birlikte sıfır olmayan max-age ve/veya s-maxage yönergesine sahip bir Cache-Control üst bilgisi belirttiniz.

Bunların her ikisi de sizin için de geçerliyse ara önbellek, gezinme isteklerine yönelik yanıtları bekletebilir. Ancak, bu kalıbı kullandığınızda herhangi bir URL için iki farklı yanıt sunuyor olabileceğinizi unutmayın:

  • Üstbilgi, içerik ve altbilgi işaretlemesini içeren tam yanıt.
  • Yalnızca içeriği içeren kısmi yanıt.

Bu durum, bazı istenmeyen davranışlara neden olarak üst bilgi ve alt bilgi işaretlemesinin iki katına çıkmasına neden olabilir. Bunun nedeni, Service Worker'ın CDN önbelleğinden tam yanıt alıp bunu önceden önbelleğe alınmış üstbilgi ve altbilgi işaretlemenizle birleştirmesidir.

Bu sorunu gidermek için Vary üstbilgisini kullanmanız gerekir. Bu üst bilgi, önbelleğe alınabilir yanıtları istekte bulunan bir veya daha fazla üstbilgiye anahtarlayarak önbelleğe alma davranışını etkiler. Gezinme isteklerine verilen yanıtları Service-Worker-Navigation-Preload ve özel X-Content-Mode istek başlıklarına göre değiştirdiğimiz için yanıtta şu Vary başlığını belirtmemiz gerekir:

Vary: Service-Worker-Navigation-Preload,X-Content-Mode

Bu üstbilgiyle tarayıcı, gezinme istekleri için tam ve kısmi yanıtları birbirinden ayırt ederek ara önbelleklerde olduğu gibi çift üstbilgi ve altbilgi işaretlemesiyle ilgili sorunları önler.

Sonuç

Çoğu yükleme süresi performans önerisi, "onlara ne elde ettiğinizi gösterin" şeklinde özetlenir. Kendinizi saklamayın, kullanıcıya bir şey göstermeden önce her şeye sahip olana kadar beklemeyin.

Jake Archibald, Daha Hızlı İçerikler İçin Eğlenceli Hacks

Tarayıcılar, çok büyük HTML yanıt gövdeleri için bile, gezinme isteklerine verilen yanıtlarla ilgilenme konusunda başarılıdır. Varsayılan olarak tarayıcılar, uzun görevlerden kaçınan işaretlemeleri kademeli olarak parçalara aktarır ve işler. Bu da başlangıç performansı için iyidir.

Akış hizmeti çalışan kalıbı kullandığımızda bu bizim yararımıza olur. En başından beri Service Worker önbelleğinden gelen bir isteğe yanıt verdiğinizde yanıtın başlangıcı neredeyse anında ulaşır. Önbelleğe alınmış üstbilgi ve altbilgi işaretlemesini ağdan gelen bir yanıtla birleştirdiğinizde önemli performans avantajları elde edersiniz:

  • Bir gezinme isteğine verilen yanıtın ilk baytı anında olduğundan İlk Bayt Süresi (TTFB) genellikle büyük ölçüde azalır.
  • İlk Zengin İçerikli Boyama (FCP) çok hızlıdır, çünkü önceden önbelleğe alınmış başlık işaretlemesi, önbelleğe alınmış bir stil sayfasına referans içerir. Bu, sayfanın çok, çok hızlı bir şekilde boyanacağı anlamına gelir.
  • Bazı durumlarda, özellikle de ekrandaki en büyük öğenin önbelleğe alınmış başlık bölümü tarafından sağlandığı durumlarda Largest Contentful Paint (LCP) de daha hızlı olabilir. Yine de daha küçük işaretleme yükleriyle birlikte mümkün olan en kısa zamanda hizmet çalışanı önbelleğinden bir şey sunmak daha iyi bir LCP sağlayabilir.

Çok sayfalı akış mimarilerini kurmak ve tekrarlamak biraz zor olabilir ancak işin karmaşık olması genellikle teoride SPA'lardan daha zahmetli değildir. Bunun temel avantajı, tarayıcının varsayılan gezinme şemasını değiştirmemek, şemayı iyileştirmek olmasıdır.

Daha da iyisi, Workbox bu mimariyi yalnızca mümkün hale getirmekle kalmıyor, aynı zamanda kendi başınıza uygulayabilmenize kıyasla daha kolay hale getiriyor. Kendi web sitenizde bu özelliği deneyin ve çok sayfalı web sitenizin bu alandaki kullanıcılar için ne kadar hızlı olabileceğini görün.

Kaynaklar