Hoppa till innehåll

Lagring i webbläsaren

Allt som finns i dina variabler försvinner när användaren laddar om sidan. Ofta gör det ingenting, eftersom datan finns på en server och hämtas igen. Men ibland finns det ingen server, eller så hör datan bara hemma hos just den här användaren: ett val av mörkt tema, ett påbörjat formulär eller topplistan i examinationsuppgift B2. Då behöver webbläsaren själv spara datan.

Den här artikeln ger en översikt över de sätt som finns och vad de har gemensamt. De två följande artiklarna går på djupet med det du behöver för B2, Web Storage, och med alternativet för större mängder data, IndexedDB.

Tidsåtgång: Ungefär 15 minuter.

Webbläsaren har flera API:er för att lagra data. De skiljer sig i hur mycket de rymmer, vad de kan lagra, om de är synkrona eller asynkrona och hur länge datan ligger kvar:

API Lagrar Rymmer Anropas Ligger kvar
localStorage strängar under en nyckel cirka 5 MiB synkront tills den tas bort
sessionStorage strängar under en nyckel cirka 5 MiB synkront tills fliken stängs
IndexedDB objekt, arrayer, datum, filer med mera gigabyte asynkront tills den tas bort
Cache API HTTP-svar, kopplade till sin förfrågan gigabyte asynkront tills den tas bort
Cookies korta strängar, skickas med till servern cirka 4 KB per cookie synkront¹ tills de går ut

¹ document.cookie är synkront. Det nyare Cookie Store API:et är asynkront.

Det första valet är oftast enkelt. Små mängder data som bara din JavaScript-kod ska läsa, som inställningar och en topplista, sparas i Web Storage. Stora mängder data, eller data du vill söka i, sparas i IndexedDB. Cache API används för att spara svar från nätverket så att en applikation fungerar utan uppkoppling, och hör hemma i progressiva webbapplikationer, som du möter från kursvecka 15. Cookies är till för servern, som du ser längre ner.

Det finns också API:er för att läsa och skriva riktiga filer. Origin Private File System (navigator.storage.getDirectory()) ger applikationen ett eget filsystem som användaren inte ser, och fungerar i alla stora webbläsare. Att låta användaren välja en fil på sin egen dator med window.showOpenFilePicker() fungerar däremot bara i Chromium-baserade webbläsare, som Chrome och Edge. Inget av dem behövs i kursen.

All lagring i webbläsaren är uppdelad per origin (ursprung), samma begrepp som du mötte i CORS i Nätverksanrop med fetch. En origin är kombinationen av schema, värdnamn och port:

https://gitlab.lnu.se:443
└─┬─┘ └─────┬─────┘ └┬┘
schema värdnamn port

Kod på en origin kan bara läsa det som har sparats av samma origin. En sida på https://example.com kan inte läsa det som https://gitlab.lnu.se har sparat i localStorage, och tvärtom. Det är det som gör det säkert att spara data i webbläsaren över huvud taget.

Uppdelningen gäller hela origin, inte en enskild sida eller applikation. Alla sidor på samma origin delar på samma localStorage. Det får en praktisk följd när du utvecklar med Vite:

Tre origins med var sin lagring. http://localhost:5173 delas av två Vite-projekt, B2-quizet och övningen Memory, som båda sparar under nyckeln scores i samma localStorage. http://127.0.0.1:5173 har en egen, tom localStorage, trots att det är samma dator och samma port. http://localhost:4173, där vite preview kör den byggda applikationen, har också en egen localStorage. En anteckning säger att schema, värdnamn och port tillsammans avgör origin, inte vilken applikation som kör.

Varje projekt du startar med npm run dev kör på http://localhost:5173. För webbläsaren är det samma origin, oavsett vilket projekt det är. Sparar både ditt quiz och ditt memoryspel något under nyckeln scores, skriver de över varandras data. Därför bör nycklarna ha ett prefix som pekar ut applikationen, som quiz:scores. Det tar nästa artikel upp.

Du startar ditt quiz med npm run dev, spelar en omgång och ser att topplistan sparas. Dagen efter öppnar du http://127.0.0.1:5173 i stället för http://localhost:5173. Topplistan är tom. Varför?

localhost och 127.0.0.1 pekar på samma dator, men de är olika värdnamn. Värdnamnet är en del av origin, så för webbläsaren är det två olika origins med var sin lagring. Topplistan finns kvar, men under http://localhost:5173.

Av samma skäl har npm run preview, som kör på port 4173, en egen lagring, skild från den du använder under utvecklingen.

Cookies är det äldsta sättet att spara data i webbläsaren, och det som skiljer sig mest från de andra. En cookie skapas oftast av servern, med headern Set-Cookie i ett svar:

HTTP/1.1 200 OK
Set-Cookie: sessionId=a3fWa; Max-Age=3600; Secure; HttpOnly; SameSite=Lax

Webbläsaren sparar cookien och skickar den sedan med i varje förfrågan till samma webbplats, i headern Cookie. Det är så en server känner igen dig när du är inloggad: varje förfrågan bär med sig samma sessionId.

Attributen efter värdet styr hur cookien hanteras:

  • Max-Age eller Expires – hur länge cookien gäller. Utan dem är det en sessionscookie, som försvinner när webbläsaren stängs.
  • Secure – skickas bara över HTTPS.
  • HttpOnly – går inte att läsa från JavaScript. Det skyddar till exempel en inloggning från ett skript som har smugits in på sidan.
  • SameSite – om cookien ska skickas med när förfrågan kommer från en annan webbplats.

JavaScript kan läsa och skriva cookies med document.cookie, men gränssnittet är egendomligt. Att läsa ger alla cookies (utom de med HttpOnly) i en enda sträng, och att tilldela lägger till eller ersätter en cookie, inte alla:

console.log(document.cookie) // "theme=dark; lang=sv"
document.cookie = 'lang=en; Max-Age=31536000; SameSite=Lax'
console.log(document.cookie) // "theme=dark; lang=en"

Det nyare Cookie Store API:et (await cookieStore.get('lang')) är asynkront och lättare att använda, och fungerar i alla stora webbläsare sedan 2025. Men för det mesta du vill spara från JavaScript är cookies fel verktyg. Varje cookie skickas med i varje förfrågan, också förfrågningar efter bilder och stilmallar, och servern har ingen nytta av ditt val av tema eller din topplista. Webbläsarna behöver bara klara 4 096 byte per cookie och 50 cookies per webbplats enligt standarden, så mer än så går inte att lita på. Spara sådant i Web Storage i stället.

Du är inloggad på en webbplats, och i utvecklarverktygen ser du en cookie sessionId. Men document.cookie i konsolen visar bara "theme=dark". Var är sessionId?

Den har attributet HttpOnly. Den skickas med i förfrågningarna till servern som vanligt, men den syns inte för JavaScript, varken via document.cookie eller Cookie Store API:et. Det är meningen: ett skript som någon har lyckats smyga in på sidan ska inte kunna läsa och skicka iväg din inloggning.

Utvecklarverktygen visar alla cookies, eftersom de inte är en del av sidan.

Det mesta som sparas i localStorage, IndexedDB och Cache API ligger kvar tills någon tar bort det. Men “någon” är inte bara din kod:

  • Användaren kan när som helst rensa webbplatsens data i webbläsarens inställningar, använda ett privat fönster, där allt försvinner när fönstret stängs, eller byta till en annan webbläsare eller dator.
  • Webbläsaren rensar själv när disken börjar bli full. Den tar då bort all data för den origin som har använts minst nyligen, först.
  • Safari tar bort data som skript har sparat för en webbplats som användaren inte har besökt och klickat på under sju dagars användning av webbläsaren.

Data i webbläsaren är alltså aldrig helt säker. Den passar för sådant som är bekvämt att ha kvar, och som går att leva utan om det försvinner. En topplista i ett quiz är ett bra exempel. Användarens enda kopia av en uppsats är det inte; den hör hemma på en server, eller i en fil som användaren själv sparar.

Varje origin har en kvot, ett tak för hur mycket den får lagra. För Web Storage är taket cirka 5 MiB per origin i alla stora webbläsare. För IndexedDB och Cache API är det mycket större, upp till en betydande del av diskens storlek, och varierar mellan webbläsarna.

Hur mycket som används och hur mycket som finns kvar kan du fråga efter med Storage Manager API:et:

const { usage, quota } = await navigator.storage.estimate()
console.log(`Used ${(usage / 1024 / 1024).toFixed(1)} MiB of ${(quota / 1024 / 1024).toFixed(0)} MiB`)

Båda värdena är i byte, och de är uppskattningar. Försöker du spara mer än kvoten tillåter kastas ett QuotaExceededError, både från localStorage och från IndexedDB.

En applikation kan också be om att få sin data skyddad mot webbläsarens egen rensning, med navigator.storage.persist(). Firefox frågar användaren, medan Chrome och Safari själva avgör efter hur mycket användaren har använt webbplatsen. Användaren kan fortfarande rensa allt själv.

Den kända cookierutan på nästan varje webbplats har sin grund i en lag, inte i tekniken. I Sverige står det i lagen (2022:482) om elektronisk kommunikation, 9 kap. 28 §, som bygger på ett EU-direktiv. Kortfattat får uppgifter lagras i eller hämtas från användarens dator eller telefon bara om användaren har fått veta vad de används till och har samtyckt. Det finns två undantag: lagring som behövs för att överföra ett meddelande, och lagring som är nödvändig för en tjänst som användaren uttryckligen har bett om.

Två saker är värda att lägga märke till:

  • Lagen gäller all lagring, inte bara cookies. Den talar om att lagra uppgifter i användarens utrustning, oavsett teknik. localStorage och IndexedDB omfattas på samma sätt.
  • Allt kräver inte samtycke. En kundvagn, en inloggning eller ett val av tema som användaren själv har gjort är nödvändiga för det användaren har bett om. Det är framför allt spårning och statistik som kräver samtycke.

Det här är en orientering, inte juridisk rådgivning. I Sverige är det Post- och telestyrelsen, PTS, som har tillsyn över reglerna.

Fråga: behöver topplistan en cookieruta?

Section titled “Fråga: behöver topplistan en cookieruta?”

Ditt quiz sparar spelarnas smeknamn och tider i localStorage, för att kunna visa en topplista på samma dator. Inget skickas till någon server. Behöver du be om samtycke?

Troligen inte. Användaren spelar quizet och förväntar sig att se topplistan; att spara resultaten är en del av det användaren har bett om, och datan lämnar aldrig datorn.

Det ändras om du lägger till något som användaren inte har bett om. Skickar du resultaten till en tjänst för statistik, eller sparar en identitet för att känna igen användaren mellan besöken, är det inte längre nödvändigt för quizet.

Allt som har sparats för en origin syns i utvecklarverktygen: under fliken Application i Chrome och Edge, och Storage i Firefox och Safari. Där finns localStorage, sessionStorage, IndexedDB, Cache Storage och cookies, var för sig. Du kan läsa, ändra och ta bort enskilda poster, och rensa allt för webbplatsen med ett klick. Det är praktiskt när du vill se vad din kod faktiskt har sparat, eller testa hur applikationen beter sig första gången den körs.

  • Små mängder data som bara JavaScript ska läsa sparas i Web Storage. Stora mängder, eller strukturerad data, sparas i IndexedDB.
  • Cookies skickas med i varje förfrågan och är till för servern. Använd dem inte för data som bara din kod behöver.
  • All lagring är uppdelad per origin: schema, värdnamn och port. Alla applikationer på samma origin, som alla Vite-projekt på localhost:5173, delar på samma lagring.
  • Data i webbläsaren kan försvinna. Spara bara sådant som det går att leva utan.
  • Varje origin har en kvot. Är den full kastas ett QuotaExceededError.
  • Lagen om samtycke gäller all lagring hos användaren, men inte det som är nödvändigt för det användaren har bett om.
CCBY