Översikt
Du fortsätter att jobba med dina PWAer och JavaScript i webbläsaren i allmänhet. Denna vecka fokuserar du på pålitliga PWAs och vad du kan göra för att optimera dina applikationer. Information om veckans upplägg annonseras löpande i kursens Zulip-kanal.
Stödjande inspelningar
Section titled “Stödjande inspelningar”Pålitliga PWA-applikationer
Section titled “Pålitliga PWA-applikationer”Varför routing i en SPA?
Section titled “Varför routing i en SPA?”I en single-page-applikation (SPA) laddas sidan aldrig om, men URL:en bör ändå spegla var i applikationen användaren befinner sig – precis som Spotifys webbspelare uppdaterar URL:en (.../artist/<id>, .../playlist/<id>) när man navigerar mellan artister, spellistor och album, utan att någonsin ladda om sidan. Det ger delbara länkar, fungerande bakåt/framåt-navigering och en begriplig historik, trots att inget nytt HTML-dokument faktiskt hämtas.
Läsa ut URL:en: window.location och URL
Section titled “Läsa ut URL:en: window.location och URL”window.location ger read-only-liknande tillgång till URL:ens olika delar: hash, host, href, pathname, port, protocol, search (query-strängen). search kommer ut som en rå sträng, vilket är omständligt att parsa för hand – URLSearchParams (nåbart via new URL(...).searchParams) löser det snyggare:
const params = new URL(document.location).searchParamsconst name = params.get('name') // strängen "Tim"const age = Number.parseInt(params.get('id')) // talet 1432, notera: alltid en sträng från börjanFragment identifier routing (hashbang) – en äldre teknik
Section titled “Fragment identifier routing (hashbang) – en äldre teknik”Innan bättre API:er fanns användes ankarlänkar (#) för att routa utan sidomladdning: eftersom webbläsaren aldrig laddar om sidan för en ren #-länk kunde man lyssna på händelsen hashchange, läsa ut window.location.hash och manuellt parsa fram vilken “sida” som avsågs (ofta med substring/split). Kombinationen #! (“hashbang”) signalerade historiskt till sökmotorer att innehållet bakom länken hämtades dynamiskt med JavaScript och borde hämtas separat för indexering – ett arv från en tid då sökmotorer hade svårt att exekvera JavaScript, betydligt mindre relevant idag men fortfarande synligt i äldre kod. Tekniken fungerar, men manuell strängparsning av hashen är klumpig och svår att bygga vidare på ju fler routes som tillkommer.
pushState/popState – det moderna sättet
Section titled “pushState/popState – det moderna sättet”window.history.pushState(stateObjekt, title, url) löser samma problem betydligt smidigare: den lägger till en post i webbläsarens historik, associerar ett godtyckligt (serialiserbart – inga funktioner) state-objekt med en given URL, och uppdaterar adressfältet – helt utan sidomladdning.
const stateObj = { accountnbr: '47435675', account: 'Account 1' }window.history.pushState(stateObj, 'Account information', '/account/47435675')Andra argumentet (titeln) respekteras i praktiken bara av Safari – Chrome och Firefox visar alltid huvuddokumentets egen <title> i historiken oavsett vad som skickas in, av skäl ingen riktigt har kunnat förklara. Argumentet måste ändå anges (kan lämnas tomt).
Motsvarande händelse popstate (lyssnas på via window.addEventListener('popstate', ...)) triggas när användaren navigerar med webbläsarens bakåt/framåt-knappar och ger tillbaka det state-objekt som hörde till den posten i historiken via event.state. Viktiga begränsningar att skydda sig mot:
event.statekan varanull(till exempel vid den allra första sidladdningen) – kräver en guard-sats innanevent.state-egenskaper läses ut.popstatetriggas inte vid en vanlig sidomladdning eller när en URL klistras in direkt i adressfältet. Det scenariot måste hanteras separat, till exempel genom att läsawindow.history.statei enload-händelselyssnare.
Serverkonfiguration krävs för att SPA-routing ska fungera överhuvudtaget
Section titled “Serverkonfiguration krävs för att SPA-routing ska fungera överhuvudtaget”Att bygga en URL-struktur på klienten löser bara halva problemet: skriver en användare in eller laddar om en URL som /account/123 direkt måste servern ändå svara med index.html (så att applikationens JavaScript kan starta och själv avgöra vad som ska visas) istället för en 404 – klienten har ingen möjlighet att fixa till detta i efterhand. Verktyget som användes vid inspelningstillfället (Snowpack 3, då i experimentellt skede) löste det via en routes-konfiguration som omdirigerar allt utan filändelse till index.html. Den exakta konfigurationssyntaxen är verktygsspecifik och föråldrad information att luta sig på idag – den bestående poängen är principen: samma omdirigeringsregel måste finnas både i den lokala utvecklingsmiljön och i produktionsmiljön (till exempel konfigurerad i nginx), annars fungerar routing lokalt men går sönder efter driftsättning.
Client-side routing är heller inget alla SPA:er behöver – vissa applikationer (till exempel en enklare examinationsuppgift utan tydligt strukturerad, navigerbar data) vinner inte särskilt mycket på det, medan andra (som Spotifys tydligt strukturerade artister/spellistor/album) passar det väl. Det är ett verktyg att ta till när det löser ett verkligt problem, inte ett krav för alla SPA:er.
navigator.onLine är otillförlitlig – “Lie-Fi”
Section titled “navigator.onLine är otillförlitlig – “Lie-Fi””window.navigator.onLine (plus händelserna online/offline) ser ut som den självklara lösningen för att avgöra om applikationen har uppkoppling, men testar i praktiken bara om enhetens nätverksgränssnitt är anslutet till något (en mobilmast, en accesspunkt) – inte om den faktiska vägen fram till servern fungerar. Fenomenet att uppleva sig som uppkopplad utan att någon data faktiskt kommer fram (fyra staplar men inget laddar) kallas “Lie-Fi”, ett begrepp som myntades på Google.
Pålitligare sätt att avgöra om servern faktiskt nås: låta en Service Worker kontrollera om ett nätverksanrop får svar inom en viss tid, sätta upp en enkel heartbeat/ping mot en egen server (till exempel via WebSocket), eller – ofta enklast – helt enkelt hantera det när ett faktiskt anrop misslyckas (till exempel ett chattmeddelande som inte går fram) snarare än att försöka förutspå uppkopplingsstatus i förväg. Hur strikt detta behöver hanteras beror på applikationen: en chattapplikation kan hantera ett enstaka misslyckat meddelande med stigande återförsöks-intervaller (vänta 10 sekunder, sedan en minut, sedan fem), medan en realtidsapplikation med höga krav på svarstid (till exempel ett multiplayer-spel) kan behöva inbyggd felhantering direkt i den ström av data som löpande skickas.
Oavsett teknik är det viktiga att kommunicera tydligt till användaren vad som händer (inaktivera ett inputfält, visa att ett meddelande väntar på att skickas) snarare än att låta appen tyst misslyckas. Ett ofta bortglömt relaterat problem: samma användare kan ha flera aktiva instanser av samma applikation samtidigt (mobil och dator), vilket kräver att man tänker igenom hur data synkas och eventuella konflikter hanteras mellan dem.
Varför ramverk finns
Section titled “Varför ramverk finns”Att hand-skriva routing genom strängparsning (substring, split, en lång kedja av if-satser) fungerar men skalar dåligt och blir svårt att underhålla. Det är precis den här typen av återkommande problem – särskilt state-hantering – som klientsideramverk (React, Vue, Svelte, Angular, Ember, Polymer med flera) löser åt en. Kursens webbkomponenter ger redan en bekant grund: koncept som “bygg i komponenter” och kapslad funktionalitet återkommer rakt av i ramverken, det är främst den mer avancerade state-hanteringen som tillkommer. Rekommendationen är att bli trygg med rena webbkomponenter innan man väljer att lägga till inlärningskurvan för ett ramverk ovanpå det, snarare än att stressa in i ett ramverk för tidigt.
Lazy loading
Section titled “Lazy loading”Vad är lazy loading?
Section titled “Vad är lazy loading?”MDN definierar lazy loading som en strategi för att identifiera resurser som icke-blockerande och inte kritiska, och bara ladda dem när de faktiskt behövs. Det är motsatsen till att ladda in allt en sida potentiellt kan behöva redan från start – en bild långt ner på sidan, en JavaScript-modul som bara används i ett sällan besökt läge, eller ett tredjepartsbibliotek som bara krävs för en liten del av funktionaliteten. Ju mer en applikation växer, desto mer data riskerar att laddas i onödan för de flesta besökare, vilket särskilt märks på enheter med sämre prestanda eller uppkoppling.
Mäta faktisk användning: Coverage-verktyget
Section titled “Mäta faktisk användning: Coverage-verktyget”Chrome DevTools har ett verktyg (Source → Coverage, aktiveras via kommandopaletten) som visar hur stor andel av laddad JavaScript och CSS som faktiskt körs respektive tillämpas på den aktuella sidan efter en omladdning. I föreläsningens exempel visade det sig att en stor andel av koden som laddades ner på en riktig webbplats – bland annat närmare 90 % av ett äldre jQuery-bibliotek och drygt 80 % av den medföljande CSS:en – aldrig användes på just den sidan som besöktes. Sådan kod behöver ändå överföras över nätet till varje besökares webbläsare, vilket vid många besökare blir en påtaglig mängd bortkastad dataöverföring – både en prestandafråga och en miljöfråga (särskilt relevant för besökare med begränsad datamängd eller uppkoppling).
Prestanda är en rankingfaktor
Section titled “Prestanda är en rankingfaktor”Google mäter sidors prestanda (historiskt via verktyget Lighthouse, numera en del av bredare mätningar som Core Web Vitals) och väger in resultatet i sökrankningen – särskilt för mobila besökare. Det gör lazy loading till mer än en teknisk optimering: en sida som laddar onödigt mycket okänd kod straffas i praktiken i sökresultaten.
Bilder: loading="lazy"
Section titled “Bilder: loading="lazy"”Det enklaste sättet att lazy-loada bilder är HTML-attributet loading="lazy", som talar om för webbläsaren att den själv får avgöra när bilden faktiskt behöver laddas (i praktiken när den närmar sig synligt område vid scroll):
<img src="bild.png" alt="…" loading="lazy" />Attributet är enkelt att lägga till på i princip alla bilder, och kraschar ingenting om webbläsaren av någon anledning inte skulle stödja det – ett obekant attributvärde ignoreras bara.
En intressant detalj är att loading="lazy" medvetet slutar fungera om JavaScript är avstängt i webbläsaren – samma princip som att JavaScript inte tillåts läsa av :visited-länkars färg. Om lazy loading av bilder fungerade även utan JavaScript skulle en sida kunna spåra ungefär hur långt en användare scrollat, genom att strategiskt placera bilder i markupen och observera vilka bildanrop som kommer in till servern och när. Det är alltså en medveten anti-spårningsåtgärd i specifikationen, inte en begränsning av misstag.
Skärmläsare och SEO-crawlers klarar sig utan särskild hänsyn
Section titled “Skärmläsare och SEO-crawlers klarar sig utan särskild hänsyn”Två vanliga farhågor kring lazy-loadat innehåll visar sig i praktiken inte vara problem längre: skärmläsare navigerar en sida i vanlig webbläsare och triggar samma scroll-baserade laddning som en seende användare gör. Och sökmotorernas crawlers (Googles i alla fall) kör numera JavaScript och simulerar även scroll nedåt på sidan, vilket innebär att innehåll som laddas lazy inte missas vid indexering – historiskt ett verkligt orosmoment för dynamiskt tillagt innehåll, men numera snarare en fördel eftersom lazy loading premieras av samma mätning som straffade det tidigare.
JavaScript-moduler: dynamisk import()
Section titled “JavaScript-moduler: dynamisk import()”Utöver import-satser i toppen av en fil finns import() som ett vanligt, asynkront funktionsanrop – tillgängligt direkt på window-objektet – för att ladda in en modul först när den faktiskt behövs:
// i en asynkron kontextconst module = await import('js/components/a-late-component/')// arbeta med modulen, t.ex. module.myFunction()
// eller med then/catchimport('js/components/a-late-component/').then(module => { // arbeta med modulen})Precis som fetch returnerar import() ett Promise som löser ut till ett modulobjekt – exporterade funktioner och värden nås som egenskaper på det objektet. Det här är ett renare sätt att skjuta upp laddning av kod än äldre tekniker som att manuellt hämta JavaScript-text med fetch och köra eval på den, eller att dynamiskt skjuta in en <script>-tagg i DOM:en. Ett naturligt användningsfall är en applikation uppbyggd kring flera separata delar (t.ex. flera “appar” i ett skrivbordsliknande gränssnitt) – precis som ett operativsystem inte laddar in alla installerade program vid uppstart, behöver inte all applikationskod laddas förrän användaren faktiskt öppnar den specifika delen.
IntersectionObserver som komplement
Section titled “IntersectionObserver som komplement”För att avgöra när något ska lazy-loadas (till exempel en tung inbäddad video, inte bara en enkel <img>) används ofta IntersectionObserver – ett inbyggt webbläsar-API för att bli notifierad när ett element blir synligt i viewporten, med möjlighet att lägga på en marginal (t.ex. ladda 300 pixlar innan elementet faktiskt syns, för att hinna med eventuell laddningstid). Att manuellt räkna ut sidstorlek och scrollposition för att avgöra vad som är synligt skulle snabbt bli komplext att implementera själv – IntersectionObserver löser samma problem deklarativt. Vid en applikation uppbyggd med dynamisk import() av separata delar (som exemplet ovan) behövs dock inte nödvändigtvis IntersectionObserver, eftersom laddningen där styrs av användarens handling (t.ex. ett klick) snarare än av vad som är synligt på skärmen.
Läsanvisningar
Section titled “Läsanvisningar”Under läsanvisningar hittar du veckans kapitel som ska läsas i kurslitteraturen. Planera upp och dela upp läsningen under veckan.
Handledning
Section titled “Handledning”Skicka gärna in frågor på förhand via formuläret som du når här:
Examinationsuppgift B3
Section titled “Examinationsuppgift B3”Inte kommit igång med uppgiften? Nu är det hög tid! Läs mer om examinationsuppgift B3.
Studiefrågor
Section titled “Studiefrågor”Här är ett antal frågor som du kan försöka besvara och diskutera tillsammans med din AI-assistent. Tveka inte att lyfta någon av frågorna till handledningen för att fortsätta diskussionen med lärare och studiekamrater.
- Vad är viktigt att tänka på när man bygger en webbapplikation som PWA?
- Hur fungerar begreppet routing och window.location i en SPA?
- Hur jobbar man med offline?
- Vad innebär lazy loading och är det relevant?
Här är en utskrift från hur det såg ut när jag diskuterade ovan frågor med min AI-assistent Ajj.
Här följer tips till hur du kan prioritera bland kursmaterialet för att komma igång med veckan.
- Börja koda B3, lös grunderna för fönsterhanteringen först, därefter lägger du till en applikation. Sedan är grunden på plats och du kan lägga till fler applikationer.
- Titta igenom föreläsningarna då de ger insyn i olika tekniker som är relevanta när man bygger en applikation som finns i B3.
