Hopp til hovedinnhold

JavaScript er i dag selve grunnmuren i moderne webutvikling. Likevel er det én helt grunnleggende del av programmering som mange fortsatt unngår å gjøre skikkelig i JavaScript-prosjekter: nemlig feilsøking. I de fleste andre språk er det selvsagt at man bruker et ordentlig feilsøkingsverktøy. I webutvikling derimot, virker det av en eller annen grunn mindre intuitivt, og det er altfor lett å ende opp med å fylle koden med console.log() for å finne ut hva som skjer.

For å forstå hvorfor det har blitt slik, må vi spole litt tilbake i historien.

JavaScript finnes ikke! Drake-meme der han er misfornøyd med et ordentlig feilsøkingsverktøy, og tilsynelatende veldig fornøyd med en enkel console-log.

I 1995 lanserte Netscape scripting-språket JavaScript i nettleseren sin, Netscape Navigator. Språket skulle gjøre det mulig å lage interaktive web-applikasjoner. Samtidig kom Microsoft med Internet Explorer, og året etter også JavaScript-ekvivalenten JScript. Det var revolusjonerende å dynamisk kunne endre innhold på siden etter den initielle innlastingen av HTML og CSS. Men selv om språkene løste en rekke problemer og åpnet for flere kreative muligheter, ble det ganske tungt for utviklere å forholde seg til to ganske forskjellige språk for forskjellige nettlesere. Derfor sendte Netscape språket sitt til ECMA (European Computer Manufacturer Association), som i 1997 lanserte den første utgaven av ECMAScript-standarden (ES). Standarden beskrev et sett med regler som alle web-scripting-språk skulle følge.

I begynnelsen ble Microsoft med på å samarbeide om å utvikle ES-standarden. Men på dette tidspunktet kunne ingen nettlesere måle seg med Microsofts Internet Explorer, som hadde en markedsandel på rundt 95%. Scripting var heller ikke særlig stort på den tiden. Ofte trengte man ikke mer enn statiske sider bestående av HTML og CSS. Microsoft sluttet derfor i praksis å videreutvikle JScript mellom 2001 og 2006. Samtidig fortsatte ECMAScript å utvikle seg, og nettlesere som Firefox og Chrome som implementerte ES-standarden ble stadig mer populære. Microsoft, som begynte å få ganske tøff konkurranse fra disse nyoppståtte nettleserne, endte derfor også opp med å implementere hele ES5, og deler av ES6. Dessverre for Microsoft, så måtte de til slutt innse at de var for sent ute. Nettleserkrigen var tapt: Internet Explorer mistet gradvis fotfeste, og markedsandelen krympet helt til de ble nødt til å avvikle nettleseren.

For å lyse opp mørketiden, skal jeg nå komme med julens største brannfakkel.

JavaScript finnes ikke!
JavaScript finnes ikke! Eller... Hvorvidt det finnes er et definisjonsspørsmål.

La oss sammenligne JavaScript med et annet kjent programmeringsspråk: Java. Java kommer som et komplett økosystem. Som utvikler laster du ned en Java Development Kit (JDK), som inkluderer ting som en kompilator, virtuell maskin og et standardbibliotek. JavaScript fungerer ikke slik. Du laster aldri ned "JavaScript". Det finnes ingen offisiell VM, ingen sentral kompilator og ingen global kodebase. Det som finnes, er ECMAScript-spesifikasjonen, en standard som beskriver hvordan språket skal oppføre seg. For eksempel:

13.8.1 The Addition Operator ( + )
The addition operator either performs string concatenation or numeric addition.

Hver nettleser implementerer så sin egen JavaScript-motor. Chrome har V8, Firefox har SpiderMonkey, og Safari har JavaScriptCore. Alle disse er egne kodebaser, og de inneholder hver sin implementasjon av ES-standarden. Det er disse JavaScript-motorene web-applikasjonen din kjører i.

ECMAScript kommer med omtrent én ny versjon i året. Standarden utvikles gjennom en firestegs prosess, der alle forslag som når siste steg, blir en del av neste utgave. Når en ny ES-versjon publiseres, kan det likevel ta tid før nettleserne faktisk støtter den. Implementeringen skjer i ulikt tempo, og det kan gå flere måneder fra en funksjon er vedtatt i standarden til den er ferdig implementert i de ulike JavaScript-motorene.

ECMAScript i midten med piler ut til Firefox, Safari og Google Chrome med hver sin JavaScript-motor. Hver pil har forskjellige tidsstempel: 4 måneder, 8 måneder og 14 måneder, for å indikere at de ulike nettleserne implementerer ES med ulik tempo
Det kan flere måneder før de ulike nettleserne implementerer en ny utgave av ES-standarden.

Men problemene stopper ikke der! Når du utvikler en Java-applikasjon, kan du pakke den inn sammen med en passende Java Runtime Environment (JRE). Applikasjonen kan så kjøres i denne JREen på sluttbrukerens PC. Du har med andre ord ganske mye kontroll over kjøretidsmiljøet for applikasjonen din, selv på brukerens maskin. Men du sender ikke nettsiden din sammen med en JavaScript-motor eller nettleser. Det er kun sluttbrukeren som har kontroll over kjøretidsmiljøet. Og det er et problem! For tror du Bertil, 78 år, kommer til å holde nettleseren sin oppdatert? Og blant annet derfor er vi som utviklere nødt til å forholde oss til slike kompatibilitetsoversikter:

Eksempel-oversikt fra MDN over funksjoner, nettlesere, og hvilke versjoner av hver nettleser som implementerer funksjonen.
Eksempel-oversikt hentet fra MDN. Viser hvilken versjon av ulike JavaScript-kjøretidsmiljøer som implementerer gitt funksjonalitet.

De minst oppdaterte, men fortsatt "støttede" nettleserne, implementerer bare ES5-standarden fra 2009. Heldigvis finnes såkalte "transpilers", som Babel og Speedy Web Compiler (SWC). Disse kan konvertere JavaScript-syntaks for en nyere ES-versjon til en eldre utgave, og for eksempel oversette ES6-kompatibel JavaScript-kode til ES5:

// === ES6 ===

const user = ["John", "Doe"];

// destrukturerer user
const [firstname, lastname] = user;

console.log(firstname); // John


// === ES5 transpilert med Babel.js ===

var user = ["John", "Doe"];

var firstname = user[0], lastname = user[1];

console.log(firstname); // John

Men noen ganger er det hele features som mangler i en eldre ES-versjon. For eksempel kom ikke fetch()-APIet før ES6 i 2015. Før det brukte man XMLHttpRequest. Så hvordan greier vi egentlig å kjøre fetch-requests i gamle nettlesere? Svaret er polyfills. En polyfill er en funksjon som implementerer et moderne API (som fetch) ved hjelp av gamle APIer (som XMLHttpRequest). Hver gang en moderne web-app kjører fetch, må en gammel nettleser istedenfor kalle en polyfill-funksjon som implementerer noe som ligner på fetch ved hjelp av funksjonalitet som den faktisk støtter.

// sjekk om fetch støttes
if (!fetch) {
  fetch = function (args) {
    // ...implementer polyfill
  }
}

Transpilere og polyfills gjør altså at vi som utviklere ikke trenger å forholde oss til gamle ES-standarder når vi utvikler.

Men som utviklere skriver vi sjeldent ES-kompatibel JavaScript. Hver dag får vi nemlig 15 nye "hippe" frontend-rammeverk som ønsker å revolusjonere web-utvikling. De kan ikke vente til den årlige utgaven av ECMAScript. De implementerer heller kontinuerlig kule standarder som å skrive HTML direkte i JavaScript-kode (JSX), eller bruke typer (TypeScript). Men Internet Explorer 💥eksploderer💥 hvis du prøver å mate den JSX. Derfor må alle rammeverk kompileres til ES-kompatibel kode som nettleseren forstår. Dette gjelder for React, Vue, Svelte, Typescript, Sass og alle andre biblioteker og språk som utviklere bruker.

JSX kompileres til ES-kompatibel JavaScript som nettleseren greier å forstå.
JSX kompileres til ES-kompatibel JavaScript som nettleseren greier å forstå.

Nettsider må lastes ned til brukerens maskin over internett, og internett, som vi alle vet, kan være treigt. Derfor er det en fordel å gjøre de nedlastbare filene så små som mulig. I tillegg til kompilering, transpilering og polyfills, har vi derfor minifisering. Dette er et steg som tar kompilert ES-kode, og fjerner alt som er unødvendig (som mellomrom og forståelige variabelnavn) for å redusere filstørrelsen så mye som mulig.

Kompilert, ES-kompatibel JavaScript-kode minifiseres for å ta minst mulig plass, for å laste inn web-applikasjonen raskere.
Kompilert, ES-kompatibel JavaScript-kode minifiseres for å ta minst mulig plass, for å laste inn web-applikasjoner raskere.

Koden du skriver i din React page.tsx blir altså MASSAKRERT av alle disse stegene som kun er til for å gjøre den kompatibel med Bertils eldgamle Netscape-versjon og trege dial-up-internett. Si meg så hvordan et feilsøkingsverktøy skal finne seg igjen i denne koden?

Where's my breakpoint, lebowski? meme med bilde av minifisert kode, som er nærmest uleselig for mennesker.

Heldigvis har vi source maps. Gitt original-filen og det transpilerte minifiserte rotet, kan ethvert JavaScript-bibliotek lage en mapping fra den originale til den genererte koden:

Bildet viser generert kode til venstre, og original-koden til høyre. Nederst ser vi et source-map. Linje 24, kolonne 0 i den genererte koden mappes her til linje 2, kolonne 0 i den originale koden
Bildet viser generert kode til venstre, og original-koden til høyre. Nederst ser vi et source-map. Linje 24, kolonne 0 i den genererte koden mappes her til linje 2, kolonne 0 i den originale koden. Lek deg med visualiseringen her: https://sokra.github.io/source-map-visualization/#babel

Men med antallet biblioteker og rammeverk som er nødt til å modifisere disse source-mappene, hender det fort at source-maps knekker, og ikke lenger greier å koble sammen generert og original kode. Er source-mappet ditt ødelagt? Lykke til med å finne ut hvilket av de 67 bibliotekene som har skylden.

Så hva er egentlig JavaScript? Er det koden som utviklere har skrevet? For guda meg, den har blitt flådd, vrengt, polyfillet, transpilert, minifisert og omstøpt til den ikke lenger er gjenkjennelig, før den i det hele tatt ender opp i nærheten av det som kalles en "JavaScript-motor". Er JavaScript koden som nettleseren forstår? Og kan vi isåfall si at vi skriver JavaScript?

Det er vanskelig å konkludere noe entydig her, men det viktigste poenget er kanskje at JavaScript-økosystemet er komplisert og består av mange bevegelige deler. Og slik har det blitt av en grunn. Det har på mange måter vært en fordel at JavaScript-motorer ikke har blitt sentralisert. En god interpreter har lenge vært et konkurransefortrinn og en viktig innovasjonsdriver for nettleserleverandører. Chrome introduserte for eksempel Just-In-Time-kompilering, og kunne kjøre JavaScript dramatisk raskere enn konkurrerende nettlesere. Slik konkurranse har ført til kontinuerlig innovasjon, som er veldig bra for sluttbrukere. I tillegg kan forskjellige motorer optimaliseres for forskjellige behov: én nettleser kan prioritere ren ytelse, mens en annen fokuserer på mobile enheter og batterisparing. Denne fleksibiliteten hadde gått tapt med én felles motor. Til slutt spiller også praktiske hensyn inn. Nettet kan være tregt, og selv om det er uproblematisk å bundle en JRE med et spill som Minecraft, ville det vært helt urealistisk å laste ned et komplett JavaScript-kjøretidsmiljø for hver nettside man besøker, spesielt for 20 år siden, da nedlastingshastigheter var på noen få kilobyte i sekundet.

Så kanskje er det et nødvendig onde at JavaScript-miljøet har blitt så komplekst som det er. Det gjør feilsøking vanskeligere, men samtidig betyr det ikke at vi mangler gode feilsøkingsverktøy. Moderne nettlesere kommer nemlig med et hav av innebygde muligheter: konsoller, verktøy for ytelsesanalyse, sporing av nettverkskall og mye mer. På serversiden er JavaScript som regel bare en Node-prosess som du kan koble deg på med hvilken som helst debugger, enten det er den som er innebygd IntelliJ, VS Code, eller noe helt annet.

Hvis du har litt tid i romjula, vil jeg anbefale at du setter deg ned og utforsker hvilke feilsøkingsverktøy som faktisk finnes for JavaScript-prosjektet ditt. Kanskje ender du opp med å bruke dem fremfor å ty til en rask console.log() neste gang du feilsøker.

Liker du innlegget?

Del gjerne med kollegaer og venner