Snellere applicaties met meerdere pagina's met streams

Tegenwoordig gebruiken websites (of web- apps als je dat liever hebt) de neiging om een ​​van de volgende twee navigatieschema's te gebruiken:

  • Het navigatieschema dat browsers standaard bieden, dat wil zeggen dat u een URL in de adresbalk van uw browser invoert en een navigatieverzoek retourneert een document als antwoord. Vervolgens klikt u op een link, die het huidige document voor een ander document ontlaadt, tot in het oneindige .
  • Het applicatiepatroon met één pagina, dat een initieel navigatieverzoek omvat om de applicatieshell te laden en afhankelijk is van JavaScript om de applicatieshell te vullen met door de klant weergegeven opmaak met inhoud van een back-end API voor elke "navigatie".

De voordelen van elke aanpak zijn aangeprezen door hun voorstanders:

  • Het navigatieschema dat browsers standaard bieden, is veerkrachtig, omdat voor routes geen JavaScript nodig is om toegankelijk te zijn. Het door de client weergeven van opmaak door middel van JavaScript kan ook een potentieel duur proces zijn, wat betekent dat lagere apparaten in een situatie terecht kunnen komen waarin inhoud wordt vertraagd omdat het apparaat wordt geblokkeerd bij het verwerken van scripts die inhoud leveren.
  • Aan de andere kant kunnen Single Page Applications (SPA's) snellere navigatie bieden na de eerste keer laden. In plaats van te vertrouwen op de browser om een ​​document te laden voor een geheel nieuw document (en dit voor elke navigatie te herhalen), kunnen ze een snellere, meer 'app-achtige' ervaring bieden, zelfs als daarvoor JavaScript nodig is.

In dit bericht gaan we het hebben over een derde methode die een balans vindt tussen de twee hierboven beschreven benaderingen: vertrouwen op een servicemedewerker om de gemeenschappelijke elementen van een website vooraf in de cache op te slaan (zoals kop- en voettekstmarkeringen) en het gebruik van streams om geef zo ​​snel mogelijk een HTML-antwoord aan de client, terwijl u nog steeds het standaardnavigatieschema van de browser gebruikt.

Waarom HTML-reacties streamen in een servicemedewerker?

Streaming is iets dat uw webbrowser al doet wanneer deze verzoeken indient. Dit is uiterst belangrijk in de context van navigatieverzoeken, omdat het ervoor zorgt dat de browser niet wordt geblokkeerd terwijl hij wacht op het volledige antwoord voordat deze kan beginnen met het parseren van documentmarkeringen en het weergeven van een pagina.

Een diagram dat niet-streaming HTML versus streaming HTML weergeeft. In het eerste geval wordt de volledige markup-payload pas verwerkt als deze arriveert. In het laatste geval wordt de opmaak stapsgewijs verwerkt wanneer deze in delen van het netwerk binnenkomt.

Voor servicemedewerkers is streaming iets anders, omdat het de JavaScript Streams API gebruikt. De belangrijkste taak die een servicemedewerker vervult, is het onderscheppen en beantwoorden van verzoeken, inclusief navigatieverzoeken.

Deze verzoeken kunnen op een aantal manieren met de cache communiceren, maar een algemeen cachepatroon voor markup is om eerst een antwoord van het netwerk te gebruiken, maar terug te vallen op de cache als er een ouder exemplaar beschikbaar is en optioneel een generieke fallback te bieden. antwoord als er geen bruikbaar antwoord in de cache aanwezig is.

Dit is een beproefd patroon voor opmaak dat goed werkt, maar hoewel het bijdraagt ​​aan de betrouwbaarheid in termen van offline toegang, biedt het geen inherente prestatievoordelen voor navigatieverzoeken die afhankelijk zijn van een netwerk eerst- of netwerkstrategie. Dat is waar streaming om de hoek komt kijken, en we zullen onderzoeken hoe u de Streams API-aangedreven workbox-streams module in uw Workbox-servicemedewerker kunt gebruiken om navigatieverzoeken op uw website met meerdere pagina's te versnellen.

Een typische webpagina opsplitsen

Structureel gezien hebben websites vaak gemeenschappelijke elementen die op elke pagina voorkomen. Een typische rangschikking van pagina-elementen gaat vaak ongeveer als volgt:

  • Koptekst.
  • Inhoud.
  • Voettekst.

Als we web.dev als voorbeeld gebruiken, ziet die uitsplitsing van algemene elementen er als volgt uit:

Een overzicht van de gemeenschappelijke elementen op de web.dev-website. De afgebakende gemeenschappelijke gebieden zijn gemarkeerd met 'koptekst', 'inhoud' en 'voettekst'.

Het doel achter het identificeren van delen van een pagina is dat we bepalen wat kan worden geprecached en opgehaald zonder naar het netwerk te gaan (namelijk de kop- en voettekstmarkeringen die voor alle pagina's gelden) en het deel van de pagina dat we altijd naar het netwerk gaan. ten eerste: de inhoud in dit geval.

Als we weten hoe we de delen van een pagina moeten segmenteren en de gemeenschappelijke elementen kunnen identificeren, kunnen we een servicemedewerker aanstellen die de kop- en voettekstmarkeringen altijd direct uit de cache haalt, terwijl hij alleen de inhoud van het netwerk opvraagt.

Vervolgens kunnen we met behulp van de Streams API via workbox-streams al deze onderdelen samenvoegen en direct reageren op navigatieverzoeken, terwijl we de minimale hoeveelheid markup die nodig is bij het netwerk vragen.

Een streamingdienstmedewerker bouwen

Er zijn veel bewegende delen als het gaat om het streamen van gedeeltelijke inhoud bij een servicemedewerker, maar elke stap van het proces zal gaandeweg in detail worden onderzocht, te beginnen met hoe u uw website structureert.

Segmenteer uw website in gedeeltelijke delen

Voordat u kunt beginnen met het schrijven van een streamingdienstmedewerker, moet u drie dingen doen:

  1. Maak een bestand dat alleen de header-opmaak van uw website bevat.
  2. Maak een bestand dat alleen de voettekstmarkeringen van uw website bevat.
  3. Haal de hoofdinhoud van elke pagina naar een apart bestand, of stel uw backend zo in dat alleen de pagina-inhoud voorwaardelijk wordt weergegeven op basis van een HTTP-verzoekheader.

Zoals je zou verwachten is de laatste stap de moeilijkste, vooral als je website statisch is. Als dat voor u het geval is, moet u twee versies van elke pagina genereren: de ene versie bevat de volledige pagina-opmaak, terwijl de andere alleen de inhoud bevat.

Een streamingdienstmedewerker samenstellen

Als u de workbox-streams module niet hebt geïnstalleerd, moet u dit doen naast de Workbox-modules die u momenteel hebt geïnstalleerd. Voor dit specifieke voorbeeld gaat het om de volgende pakketten:

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

Vanaf hier is de volgende stap het maken van uw nieuwe servicemedewerker en het vooraf cachen van uw gedeeltelijke kop- en voettekst.

Gedeeltelijke delen vooraf cachen

Het eerste dat u gaat doen, is een servicemedewerker maken in de hoofdmap van uw project met de naam sw.js (of welke bestandsnaam u ook verkiest). Daarin begin je met het volgende:

// 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...

Deze code doet een aantal dingen:

  1. Maakt het vooraf laden van navigatie mogelijk voor browsers die dit ondersteunen .
  2. Hiermee wordt de kop- en voettekstopmaak vooraf in de cache opgeslagen. Dit betekent dat de kop- en voettekstmarkeringen voor elke pagina onmiddellijk worden opgehaald, omdat deze niet door het netwerk worden geblokkeerd.
  3. Plaatst statische assets vooraf in de tijdelijke aanduiding __WB_MANIFEST die de methode injectManifest gebruikt.

Reacties streamen

Het grootste deel van deze hele inspanning is om uw servicemedewerker samengevoegde reacties te laten streamen. Toch maken Workbox en zijn workbox-streams dit een veel beknopter aangelegenheid dan wanneer je dit allemaal alleen zou moeten doen:

// 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.

Deze code bestaat uit drie hoofdonderdelen die aan de volgende eisen voldoen:

  1. Er wordt een NetworkFirst strategie gebruikt om aanvragen voor gedeeltelijke inhoud af te handelen. Met behulp van deze strategie wordt een aangepaste cachenaam van content gespecificeerd om de gedeeltelijke inhoud te bevatten, evenals een aangepaste plug-in die bepaalt of een X-Content-Mode verzoekheader moet worden ingesteld voor browsers die het vooraf laden van navigatie niet ondersteunen (en daarom niet geen Service-Worker-Navigation-Preload header verzenden). Deze plug-in zoekt ook uit of de laatste in de cache opgeslagen versie van een gedeeltelijke inhoud moet worden verzonden, of een offline reservepagina moet worden verzonden in het geval dat er geen in de cache opgeslagen versie voor het huidige verzoek is opgeslagen.
  2. De strategy in workbox-streams (hier ook wel composeStrategies genoemd) wordt gebruikt om de vooraf in de cache opgeslagen gedeeltelijke kop- en voettekst samen te voegen met de gedeeltelijke inhoud die is opgevraagd bij het netwerk.
  3. Het hele schema is opgetuigd via registerRoute voor navigatieverzoeken.

Met deze logica op zijn plaats hebben we streamingreacties ingesteld. Het kan echter zijn dat u wat werk aan de achterkant moet doen om ervoor te zorgen dat de inhoud van het netwerk een gedeeltelijke pagina is die u kunt samenvoegen met de vooraf in de cache opgeslagen gedeeltelijke pagina's.

Als uw website een backend heeft

U zult zich herinneren dat wanneer vooraf laden van navigatie is ingeschakeld, de browser een Service-Worker-Navigation-Preload header verzendt met de waarde true . In het bovenstaande codevoorbeeld hebben we echter een aangepaste header van X-Content-Mode verzonden voor het geval dat het vooraf laden van de navigatie niet wordt ondersteund in een browser. Aan de achterkant zou je het antwoord wijzigen op basis van de aanwezigheid van deze headers. In een PHP-backend zou dat er voor een bepaalde pagina ongeveer zo uit kunnen zien:

<?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();
}
?>

In het bovenstaande voorbeeld worden de gedeeltelijke inhoudselementen aangeroepen als functies, die de waarde $isPartial aannemen om te wijzigen hoe de gedeeltelijke waarden worden weergegeven. De functie voor het renderen van content kan bijvoorbeeld alleen bepaalde opmaak in voorwaarden opnemen wanneer deze als gedeeltelijk wordt opgehaald, iets dat binnenkort zal worden besproken.

Overwegingen

Voordat u een servicemedewerker inzet om gedeeltelijke onderdelen te streamen en samen te voegen, zijn er enkele zaken waarmee u rekening moet houden. Hoewel het waar is dat het gebruik van een servicemedewerker op deze manier het standaardnavigatiegedrag van de browser niet fundamenteel verandert, zijn er enkele zaken die u waarschijnlijk moet aanpakken.

Pagina-elementen bijwerken tijdens het navigeren

Het lastigste deel van deze aanpak is dat sommige dingen op de client moeten worden bijgewerkt. Het vooraf cachen van header-opmaak betekent bijvoorbeeld dat de pagina dezelfde inhoud in het <title> -element zal hebben, of dat zelfs het beheren van aan/uit-statussen voor navigatie-items bij elke navigatie moet worden bijgewerkt. Deze zaken (en andere) moeten mogelijk voor elk navigatieverzoek op de client worden bijgewerkt.

De manier om dit te omzeilen zou kunnen zijn door een inline <script> -element te plaatsen in het deel van de inhoud dat afkomstig is van het netwerk om een ​​paar belangrijke dingen bij te werken:

<!-- 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>

Dit is slechts één voorbeeld van wat u mogelijk moet doen als u besluit voor deze servicemedewerker-configuratie te kiezen. Voor complexere toepassingen met gebruikersinformatie moet u bijvoorbeeld mogelijk stukjes relevante gegevens opslaan in een webwinkel zoals localStorage en van daaruit de pagina bijwerken.

Omgaan met trage netwerken

Een nadeel van het streamen van antwoorden met behulp van markup uit de precache kan optreden wanneer netwerkverbindingen traag zijn. Het probleem is dat de header-opmaak uit de precache onmiddellijk arriveert, maar het kan enige tijd duren voordat de gedeeltelijke inhoud van het netwerk arriveert na de eerste verf van de header-opmaak.

Dit kan voor een verwarrende ervaring zorgen, en als netwerken erg traag zijn, kan het zelfs lijken alsof de pagina kapot is en niet verder wordt weergegeven. In dergelijke gevallen kunt u ervoor kiezen om een ​​laadpictogram of bericht in de gedeeltelijke opmaak van de inhoud te plaatsen, dat u kunt verbergen zodra de inhoud is geladen.

Een manier om dit te doen is via CSS. Stel dat uw header gedeeltelijk eindigt met een openingselement <article> dat leeg is totdat de gedeeltelijke inhoud arriveert om het te vullen. U zou een CSS-regel kunnen schrijven die er ongeveer zo uitziet:

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

Dit werkt, maar er wordt een laadbericht op de client weergegeven, ongeacht de netwerksnelheid. Als je een vreemde flits van berichten wilt vermijden, kun je deze aanpak proberen, waarbij we de selector in het bovenstaande fragment binnen een slow klasse nesten:

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

Vanaf hier kunt u JavaScript gedeeltelijk in uw header gebruiken om het effectieve verbindingstype te lezen (tenminste in Chromium-browsers) om de slow klasse toe te voegen aan het <html> -element bij bepaalde verbindingstypen:

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

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

Dit zorgt ervoor dat effectieve verbindingstypen die langzamer zijn dan het 4g type een laadbericht krijgen. Vervolgens kunt u in de gedeeltelijke inhoud een inline <script> -element plaatsen om de slow klasse uit de HTML te verwijderen en het laadbericht te verwijderen:

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

Het bieden van een terugvalreactie

Stel dat u een netwerk-eerst-strategie gebruikt voor uw gedeeltelijke inhoud. Als de gebruiker offline is en naar een pagina gaat waar hij al is geweest, valt hij onder de dekking. Als ze echter naar een pagina gaan waar ze nog niet zijn geweest, krijgen ze niets. Om dit te voorkomen, moet u een noodreactie indienen.

De code die nodig is om een ​​terugvalreactie te bereiken, wordt gedemonstreerd in eerdere codevoorbeelden. Het proces vereist twee stappen:

  1. Precache een offline fallback-antwoord.
  2. Stel een handlerDidError callback in de plug-in in voor uw netwerk-eerst-strategie om de cache te controleren op de laatst geopende versie van een pagina. Als de pagina nooit is geopend, moet u de matchPrecache methode uit de workbox-precaching -module gebruiken om het terugvalantwoord uit de precache op te halen.

Caching en CDN's

Als u dit streamingpatroon gebruikt bij uw servicemedewerker, beoordeel dan of het volgende op uw situatie van toepassing is:

  • U gebruikt een CDN of een ander soort tussenliggende/openbare cache.
  • U heeft een Cache-Control header gespecificeerd met een max-age en/of s-maxage richtlijn(en) die niet nul zijn, in combinatie met de public richtlijn .

Als beide voor u het geval zijn, kan de tussenliggende cache antwoorden op navigatieverzoeken vasthouden. Houd er echter rekening mee dat wanneer u dit patroon gebruikt, u mogelijk twee verschillende antwoorden voor een bepaalde URL weergeeft:

  • Het volledige antwoord, met de kop-, inhoud- en voettekstopmaak.
  • Het gedeeltelijke antwoord, dat alleen de inhoud bevat.

Dit kan ongewenst gedrag veroorzaken, wat resulteert in dubbele kop- en voettekstopmaak, omdat de servicemedewerker mogelijk een volledig antwoord ophaalt uit de CDN-cache en dat combineert met uw vooraf in de cache opgeslagen kop- en voettekstopmaak.

Om dit te omzeilen, moet u vertrouwen op de Vary header , die het cachegedrag beïnvloedt door cachebare antwoorden in te voeren op een of meer headers die aanwezig waren in het verzoek. Omdat we de reacties op navigatieverzoeken variëren op basis van de Service-Worker-Navigation-Preload en aangepaste X-Content-Mode verzoekheaders, moeten we deze Vary header in het antwoord specificeren:

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

Met deze header maakt de browser onderscheid tussen volledige en gedeeltelijke antwoorden op navigatieverzoeken, waardoor problemen met dubbele kop- en voettekstopmaak worden vermeden, net als eventuele tussenliggende caches.

De uitkomst

Het meeste prestatieadvies tijdens het laden komt neer op 'laat zien wat je hebt': houd je niet in, wacht niet tot je alles hebt voordat je de gebruiker iets laat zien.

Jake Archibald in leuke hacks voor snellere inhoud

Browsers blinken uit als het gaat om het omgaan met antwoorden op navigatieverzoeken, zelfs voor enorme HTML-antwoordlichamen. Standaard streamen en verwerken browsers de markeringen geleidelijk in stukjes, waardoor lange taken worden vermeden, wat goed is voor de opstartprestaties.

Dit werkt in ons voordeel wanneer we een werkpatroon voor streamingdiensten gebruiken. Telkens wanneer u vanaf het begin op een verzoek uit de cache van de servicemedewerker reageert, arriveert het begin van het antwoord vrijwel onmiddellijk. Wanneer u vooraf in de cache opgeslagen kop- en voettekstmarkeringen samenvoegt met een reactie van het netwerk, krijgt u een aantal opmerkelijke prestatievoordelen:

  • De Time to First Byte (TTFB) wordt vaak aanzienlijk verkort, omdat de eerste byte van het antwoord op een navigatieverzoek onmiddellijk is.
  • First Contentful Paint (FCP) zal erg snel zijn, omdat de vooraf in de cache opgeslagen header-opmaak een verwijzing naar een in de cache opgeslagen stijlblad zal bevatten, wat betekent dat de pagina zeer, zeer snel zal tekenen.
  • In sommige gevallen kan Largest Contentful Paint (LCP) ook sneller zijn, vooral als het grootste schermelement wordt geleverd door de vooraf in de cache opgeslagen gedeeltelijke header. Toch kan het zo snel mogelijk aanbieden van iets uit de cache van de servicewerknemer in combinatie met kleinere markup-payloads resulteren in een beter LCP.

Het streamen van architecturen met meerdere pagina's kan een beetje lastig zijn om op te zetten en te herhalen, maar de complexiteit die ermee gemoeid is, is in theorie vaak niet zwaarder dan bij SPA's. Het belangrijkste voordeel is dat u het standaardnavigatieschema van de browser niet vervangt, maar verbetert .

Beter nog: Workbox maakt deze architectuur niet alleen mogelijk, maar ook eenvoudiger dan wanneer u dit zelf zou implementeren. Probeer het eens op uw eigen website en kijk hoeveel sneller uw website met meerdere pagina's kan zijn voor gebruikers in het veld.

Bronnen