Skip to content

Allt om WordPress, webbutveckling — och mer därtill

⚡ Hur du laddar extern JavaScript utan att blockera sidan

⚡ Hur du laddar extern JavaScript utan att blockera sidan

När webbläsaren stöter på en <script>-tagg utan attribut släpper den allt. Renderingen av sidan stannar helt tills skriptet har laddats och körts. På en långsam 4G-uppkoppling innebär det 2-3 sekunder med en tom skärm.

Användaren har redan gått till en konkurrent vid det laget. Core Web Vitals registrerar ett misslyckat LCP, Google trycker ner sidan i sökresultaten och du tappar trafik och konverteringar. Samtidigt kan problemet lösas med tre rader om du vet var du ska leta.

Här är en fungerande metod för att ladda extern JavaScript utan blockering. Från den klassiska tvåfilsmetoden till modern async/defer och dynamisk import(). Med beprövad kod du kan kopiera och klistra in.

💡 Snabb översikt:

  • Förstå problemet: hur en vanlig <script>-tagg blockerar HTML-tolkningen och dödar laddningshastigheten
  • Bemästra den klassiska metoden: en liten laddare (≤300 byte) hämtar dynamiskt in huvud-JS-filen
  • Lär dig de inbyggda attributen async och defer: när och vilken du ska använda
  • Utforska dynamisk import() för att ladda moduler vid behov
  • Välj en strategi för ditt projekt med hjälp av en jämförelsetabell

Varför JavaScript blockerar renderingen

När HTML-tolkaren når <script src="app.js"> gör den exakt tre saker: slutar tolka dokumentet, laddar ner filen, kör den. Först därefter fortsätter den att bygga upp DOM:en.

Orsaken är arkitektonisk. Ett skript kan innehålla document.write(), som ändrar HTML i farten. Webbläsaren vet inte i förväg om det finns ett sådant anrop, så den tar det säkra före det osäkra och väntar på fullständig laddning och exekvering. Resultatet: även ett lätt 5 KB-skript lägger till hundratals millisekunder på First Contentful Paint enbart från en enda nätverksrunda.

Problemet är inte nytt. Redan 2009 beskrev Nicholas Zakas en teknik för dynamisk, icke-blockerande JavaScript-laddning, och den fungerar fortfarande idag, om än med anpassningar för moderna API:er. Med ankomsten av async, defer och ES-moduler har utvecklare nu en hel verktygslåda. Låt oss granska var och en.

Klassisk metod: två filer och dynamisk laddning

Idén är enkel. Istället för att lägga all din JS i en fil och koppla den till sidan via <script src="..."> delar du upp koden i två delar:

  • En liten laddare (200-300 byte efter komprimering)
  • Huvudfilen med applikationslogik

Laddaren placeras inline längst ner på sidan, precis före </body>. Den skapar en <script>-tagg programmatiskt och lägger till den i DOM:en, en sådan tagg blockerar inte längre tolkningen eftersom den dyker upp utanför dokumentets huvudflöde. Så snart huvudfilen laddas körs initieringen.

Modern version av funktionen i ren JS utan bakåtkompatibilitet med IE:

1function loadScript(url) {
2 return new Promise((resolve, reject) => {
3 const script = document.createElement('script');
4 script.src = url;
5 script.onload = resolve;
6 script.onerror = reject;
7 document.head.appendChild(script);
8 });
9}

Nio rader. Inga readyState-kontroller, inga grenar för gamla IE, ingen pyramid av callback-helveten. Bara en funktion som returnerar ett Promise, praktiskt att kombinera med async/await.

Användning på sidan ser ut så här (kod längst ner, före den avslutande </body>):

1<script>
2 function loadScript(url) {
3 return new Promise((resolve, reject) => {
4 const script = document.createElement('script');
5 script.src = url;
6 script.onload = resolve;
7 script.onerror = reject;
8 document.head.appendChild(script);
9 });
10 }
11
12 loadScript('/js/app.js').then(() => {
13 // Initialize after main file loads
14 App.init();
15 });
16</script>

Det första skriptet (inline) är laddaren. Det tolkas och körs direkt eftersom det är mindre än 300 byte. Det andra skriptet (app.js) laddas asynkront och stör inte renderingen.

Vad gör du om du har fler än två filer? Kombinera dem under bygget. Moderna paketerare som Vite och Webpack gör detta automatiskt: tree-shaking, code splitting, minifiering i en enda körning. Att manuellt hantera laddningsordningen för ett dussin filer är en väg mot race conditions och fel.

Async och defer: inbyggd avblockering

HTML5 gav oss två attribut som löser problemet utan en enda rad JavaScript:

1<script async src="analytics.js"></script>
2<script defer src="app.js"></script>

Båda laddar filen parallellt med HTML-tolkningen. Skillnaden ligger i tidpunkten för exekveringen:

Attribut

Laddning

Exekvering

Ordning

async

Parallellt med tolkning

Omedelbart efter laddning

Inte garanterad

defer

Parallellt med tolkning

Efter fullständig HTML-tolkning

Garanterad (som i dokumentet)

Tumregel:

  • async för oberoende skript: analys, annonser, räknare. De behöver inte DOM:en, de bryr sig inte om ordning.
  • defer för huvudapplikationen: DOM-manipulation, gränssnittsinitiering. Skriptet väntar på att sidan är redo och körs i rätt sekvens.

I praktiken är kombinationen enkel: lägg defer på alla skript i <head>, så beter de sig som om de ligger längst ner på sidan men laddas tidigare. Ingen magi, bara webbläsarens schemaläggare.

Och ja, du kan kombinera det med dynamisk laddning. Ladda till exempel applikationens kärna via <script defer>, och koppla in tunga widgets dynamiskt genom loadScript() endast när de faktiskt behövs.

Dynamisk import(): moduler vid behov

ES2020 gav oss dynamisk import(), ett inbyggt sätt att ladda en modul asynkront, utan paketerare och utan extra funktioner:

1// Loads only when user clicked
2button.addEventListener('click', async () => {
3 const { heavyChart } = await import('./chart-component.js');
4 heavyChart.render();
5});

Anropet import() returnerar ett Promise. Modulen laddas i bakgrunden, tolkningen blockeras inte, sidan förblir responsiv. Kod inuti modulen körs i strict mode och i sitt eget scope, namnkrockar elimineras.

Detta är det ideala verktyget för code splitting utan paketerare. Tunga komponenter (diagram, redigerare, kartor) flyttas till separata filer och laddas vid första interaktion. En användare som aldrig öppnar ett diagram betalar inte för det med trafik och laddningstid.

Jämförelse av metoderna

Varje metod har sin nisch. För att slippa gissa sammanställde vi egenskaperna i en tabell:

Metod

Blockerar rendering

Kräver JS

Exekveringsordning

För vilka skript

<script src>

Ja

Nej

Garanterad

Används inte om det inte är nödvändigt

Dynamisk loadScript

Nej

Ja

Via .then()-kedja

Villkorlig laddning, tunga beroenden

<script async>

Nej

Nej

Inte garanterad

Analys, annonser, räknare

<script defer>

Nej

Nej

Garanterad

Huvudapplikation, DOM-manipulation

import()

Nej

Ja (ES-modul)

Via await

Code splitting, komponenter vid behov

Huvudpoäng: fastna inte vid en metod. En typisk produktionsuppsättning använder två eller tre samtidigt: defer för kärnan, async för mätvärden, dynamisk import() för tunga komponenter.

Kort demovideo i ämnet, genomgång av async och defer med visualisering av laddningstidslinjen:

⁉️🤔 Vanliga frågor

Hur skiljer sig async från defer i praktiken?

Båda blockerar inte tolkningen under laddning. Men async kör skriptet omedelbart efter att filen har laddats, även om HTML inte är helt tolkad än, och utan ordningsgaranti. defer väntar alltid på full DOM-beredskap och bevarar skriptsekvensen som i HTML. För huvudapplikationskod, använd defer, för isolerade räknare, använd async.

Kan man kombinera dynamisk laddning med defer?

Ja, detta är ett vanligt scenario. Applikationens kärna laddas med defer i <head>, den initierar gränssnittet. Tunga eller sällan använda moduler hämtas via dynamisk loadScript() eller import() vid användarinteraktion. På så sätt får du både snabb uppstart och uppskjuten laddning av sekundär kod.

Vad ska jag använda för en WordPress-sajt?

WordPress lägger automatiskt till defer eller async via wp_enqueue_script() om du skickar med rätt argument i den femte parametern: wp_enqueue_script('my-script', $url, [], null, ['strategy' => 'defer']). För tredjepartsskript (Google Analytics, annonser) är den enklaste metoden attributet async. Komplexa interaktiva block (kalkylatorer, filter) bör flyttas till dynamisk import() inuti en anpassad modul.

Fungerar detta med tredjepartsskript som Google Analytics?

Ja. GA4-taggen gtag.js laddas med async som standard, så den blockerar inte sidan. För andra tredjepartstjänster, kontrollera dokumentationen: om skriptet inte kräver en redo DOM och inte är beroende av laddningsordning, använd gärna async. Om det behöver DOM:en, använd defer eller dynamisk laddning med en callback.

Hur verifierar jag att ett skript verkligen inte blockerar sidan?

Öppna Chrome DevTools → Prestanda → Spela in → ladda om sidan. På tidslinjen, leta efter gula "Scripting"-block före den gröna "First Contentful Paint". Om ett skript laddas med defer eller async kommer dess exekvering att ligga efter FCP. Lighthouse i "Prestanda"-läge visar rekommendationen "Ta bort renderingsblockerande resurser", det bör inte finnas några skript i den listan.

Är det värt att ändra din laddningsmetod just nu

Om dina skript fortfarande hänger i <head> utan attribut förlorar du sökrankning och irriterar användare. Detta är ingen hypotes, Lighthouse och PageSpeed Insights visar problemet i rött på rapportens första rader.

Snabbstart: gå igenom <script>-taggarna i din mall, lägg till defer för huvudkod och async för mätvärden. Det tar fem minuter, och LCP kan förbättras med 300-500 ms. Nästa steg, dynamisk import() för tunga komponenter när du får tid att refaktorera.

Lämna en laddningsmetod, <script defer> i <head>, så laddas sidan utan synliga fördröjningar. Testa det på ditt projekt idag.