Skip to content

Kaikki WordPressistä, web-kehityksestä — ja paljon muuta

⚡ Kuinka ladata ulkoinen JavaScript estämättä sivua

⚡ Kuinka ladata ulkoinen JavaScript estämättä sivua

Kun selain kohtaa <script>-tagin ilman attribuutteja, se keskeyttää kaiken muun. Sivun renderöinti pysähtyy täysin, kunnes skripti on latautunut ja suoritettu. Hitaalla 4G-yhteydellä se tarkoittaa 2-3 sekuntia tyhjää ruutua.

Käyttäjä on siinä vaiheessa jo siirtynyt kilpailijalle. Core Web Vitals kirjaa epäonnistuneen LCP:n, Google pudottaa sivua hakutuloksissa, ja menetät liikennettä ja konversioita. Samaan aikaan ongelman voi ratkaista kolmella rivillä, jos tietää mistä katsoa.

Alla on toimiva tapa ladata ulkoinen JavaScript ilman blokkausta. Klassisesta kahden tiedoston lähestymistavasta moderniin async/defer-attribuuttiin ja dynaamiseen import()-kutsuun. Mukana testattua koodia, jonka voit kopioida ja liittää.

💡 Nopea yleiskatsaus:

  • Ymmärrä ongelma: miten tavallinen <script> blokkaa HTML:n jäsentämisen ja tuhoaa latausnopeuden
  • Hallitse klassinen lähestymistapa: pieni lataaja (≤300 tavua) hakee pää-JS-tiedoston dynaamisesti
  • Opi natiivit async- ja defer-attribuutit: milloin ja kumpaa käyttää
  • Tutustu dynaamiseen import()-kutsuun moduulien lataamiseksi tarpeen mukaan
  • Valitse strategia projektillesi vertailutaulukon avulla

Miksi JavaScript blokkaa renderöinnin

Kun HTML-jäsennin saavuttaa kohdan <script src="app.js">, se tekee tasan kolme asiaa: lopettaa dokumentin jäsentämisen, lataa tiedoston ja suorittaa sen. Vasta sen jälkeen se jatkaa DOM-puun rakentamista.

Syy on arkkitehtuurissa. Skripti voi sisältää document.write()-kutsun, joka muuttaa HTML:ää lennossa. Selain ei tiedä etukäteen, onko tällaista kutsua, joten se varmuuden vuoksi odottaa täyttä latausta ja suoritusta. Lopputulos: jopa kevyt 5 kt:n skripti lisää satoja millisekunteja First Contentful Paint -arvoon jo pelkän yhden verkkokierroksen takia.

Ongelma ei ole uusi. Jo vuonna 2009 Nicholas Zakas kuvasi tekniikan dynaamiseen, ei-blokkaavaan JavaScriptin lataamiseen, ja se toimii yhä tänään, tosin modernien rajapintojen mukaisilla säädöillä. async- ja defer-attribuuttien sekä ES-moduulien myötä kehittäjillä on nyt kokonainen työkalupakki. Tarkastellaan kutakin.

Klassinen lähestymistapa: kaksi tiedostoa ja dynaaminen lataus

Idea on yksinkertainen. Sen sijaan, että laittaisit kaiken JS:n yhteen tiedostoon ja liittäisit sen sivulle <script src="...">-tagilla, jaat koodin kahteen osaan:

  • Pieni lataaja (200-300 tavua pakattuna)
  • Päätiedosto, jossa on sovelluslogiikka

Lataaja lisätään inline-muodossa sivun loppuun, juuri ennen </body>-tagia. Se luo <script>-tagin ohjelmallisesti ja lisää sen DOM-puuhun, tällainen tagi ei enää blokkaa jäsentämistä, koska se ilmestyy päädokumenttivuon ulkopuolelle. Heti kun päätiedosto latautuu, alustus suoritetaan.

Funktion moderni versio puhtaalla JS:llä ilman IE-taaksepäin yhteensopivuutta:

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}

Yhdeksän riviä. Ei readyState-tarkistuksia, ei haaroja vanhalle IE:lle, ei callback-helvettiä. Vain funktio, joka palauttaa Promisen, kätevä yhdistettäväksi async/await-rakenteeseen.

Käyttö sivulla näyttää tältä (koodi alaosassa, ennen sulkevaa </body>-tagia):

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>

Ensimmäinen skripti (inline) on lataaja. Se jäsennetään ja suoritetaan välittömästi, koska se on alle 300 tavua. Toinen skripti (app.js) latautuu asynkronisesti eikä häiritse renderöintiä.

Entä jos sinulla on enemmän kuin kaksi tiedostoa? Yhdistä ne build-vaiheessa. Modernit paketointityökalut kuten Vite ja Webpack tekevät tämän automaattisesti: tree-shaking, koodin jakaminen, minifiointi yhdellä kertaa. Kymmenen tiedoston latausjärjestyksen manuaalinen hallinta on tie kilpailutilanteisiin ja virheisiin.

Async ja defer: natiivi blokkauksen poisto

HTML5 antoi meille kaksi attribuuttia, jotka ratkaisevat ongelman ilman yhtäkään JavaScript-riviä:

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

Molemmat lataavat tiedoston rinnakkain HTML:n jäsentämisen kanssa. Ero on suorituksen ajoituksessa:

Attribuutti

Lataus

Suoritus

Järjestys

async

Rinnakkain jäsentämisen kanssa

Välittömästi latauksen jälkeen

Ei taattu

defer

Rinnakkain jäsentämisen kanssa

Täyden HTML-jäsentämisen jälkeen

Taattu (kuten dokumentissa)

Nyrkkisääntö:

  • async riippumattomille skripteille: analytiikka, mainokset, laskurit. Ne eivät tarvitse DOM-puuta, eivätkä välitä järjestyksestä.
  • defer pääsovellukselle: DOM-manipulaatio, käyttöliittymän alustus. Skripti odottaa sivun valmiutta ja suoritetaan oikeassa järjestyksessä.

Käytännössä yhdistelmä on yksinkertainen: laita defer kaikkiin <head>-osion skripteihin, ja ne käyttäytyvät kuin ne olisivat sivun alaosassa, mutta latautuvat aiemmin. Ei taikuutta, vain selaimen ajastin.

Ja kyllä, voit yhdistää sen dynaamiseen lataukseen. Lataa esimerkiksi sovelluksen ydin <script defer> -tagilla ja liitä raskaat widgetit dynaamisesti loadScript()-funktiolla vain silloin, kun niitä todella tarvitaan.

Dynaaminen import(): moduulit tarpeen mukaan

ES2020 toi dynaamisen import()-kutsun, natiivin tavan ladata moduuli asynkronisesti, ilman paketointityökalua ja ilman ylimääräisiä funktioita:

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

import()-kutsu palauttaa Promisen. Moduuli latautuu taustalla, jäsentäminen ei blokkaudu, sivu pysyy responsiivisena. Moduulin sisällä oleva koodi suoritetaan strict-tilassa ja omassa näkyvyysalueessaan, nimikonfliktit on eliminoitu.

Tämä on ihanteellinen työkalu koodin jakamiseen ilman paketointityökalua. Raskaat komponentit (kaaviot, editorit, kartat) siirretään erillisiin tiedostoihin ja ladataan ensimmäisellä interaktiolla. Käyttäjä, joka ei koskaan avaa kaaviota, ei maksa siitä liikenteellä ja latausajalla.

Lähestymistapojen vertailu

Jokaisella menetelmällä on oma paikkansa. Arvailun välttämiseksi kokosimme ominaisuudet taulukkoon:

Lähestymistapa

Blokkaa renderöinnin

Vaatii JS:n

Suoritusjärjestys

Mille skripteille

<script src>

Kyllä

Ei

Taattu

Ei käytetä ellei ole pakko

Dynaaminen loadScript

Ei

Kyllä

.then()-ketjun kautta

Ehdollinen lataus, raskaat riippuvuudet

<script async>

Ei

Ei

Ei taattu

Analytiikka, mainokset, laskurit

<script defer>

Ei

Ei

Taattu

Pääsovellus, DOM-manipulaatio

import()

Ei

Kyllä (ES-moduuli)

await-kutsun kautta

Koodin jakaminen, tarpeen mukaan ladattavat komponentit

Pääopetus: älä jumitu yhteen menetelmään. Tyypillinen tuotantokokoonpano käyttää kahta tai kolmea samanaikaisesti: defer ytimelle, async metriikalle, dynaaminen import() raskaille komponenteille.

Lyhyt demovideo aiheesta, async- ja defer-attribuuttien erittely ja latausaikajanan visualisointi:

⁉️🤔 Usein kysytyt kysymykset

Miten async eroaa defer-attribuutista käytännössä?

Molemmat eivät blokkaa jäsentämistä latauksen aikana. Mutta async suorittaa skriptin heti tiedoston latauduttua, vaikka HTML ei olisi vielä täysin jäsennetty, eikä takaa järjestystä. defer odottaa aina täyttä DOM-valmiutta ja säilyttää skriptien järjestyksen kuten HTML:ssä. Pääsovelluskoodille käytä defer-attribuuttia, eristetyille laskureille async-attribuuttia.

Voiko dynaamisen latauksen yhdistää defer-attribuuttiin?

Kyllä, tämä on yleinen skenaario. Sovelluksen ydin latautuu defer-attribuutilla <head>-osiossa, se alustaa käyttöliittymän. Raskaat tai harvoin käytetyt moduulit haetaan dynaamisella loadScript()- tai import()-kutsulla käyttäjän interaktion yhteydessä. Näin saat sekä nopean käynnistyksen että toissijaisen koodin viivästetyn latauksen.

Mitä minun pitäisi käyttää WordPress-sivustolla?

WordPress lisää automaattisesti defer- tai async-attribuutin wp_enqueue_script()-funktion kautta, jos välität sopivan argumentin viidennessä parametrissa: wp_enqueue_script('my-script', $url, [], null, ['strategy' => 'defer']). Kolmannen osapuolen skripteille (Google Analytics, mainokset) helpoin tapa on async-attribuutti. Monimutkaiset interaktiiviset lohkot (laskurit, suodattimet) kannattaa siirtää dynaamiseen import()-kutsuun mukautetun moduulin sisällä.

Toimiiko tämä kolmannen osapuolen skriptien, kuten Google Analyticsin, kanssa?

Kyllä. GA4-tagi gtag.js latautuu oletuksena async-attribuutilla, joten se ei blokkaa sivua. Muiden kolmannen osapuolen palveluiden kohdalla tarkista dokumentaatio: jos skripti ei vaadi valmista DOM-puuta eikä ole riippuvainen latausjärjestyksestä, voit huoletta käyttää async-attribuuttia. Jos se tarvitsee DOM-puun, käytä defer-attribuuttia tai dynaamista latausta callback-funktiolla.

Miten varmistan, ettei skripti todella blokkaa sivua?

Avaa Chrome DevTools → Performance → Record → päivitä sivu. Aikajanalla etsi keltaisia "Scripting"-lohkoja ennen vihreää "First Contentful Paint" -merkintää. Jos skripti on ladattu defer- tai async-attribuutilla, sen suoritus on FCP:n jälkeen. Lighthouse "Performance"-tilassa näyttää "Remove render-blocking resources" -suosituksen, listalla ei pitäisi olla skriptejä.

Kannattaako lataustapaa vaihtaa juuri nyt

Jos skriptisi roikkuvat yhä <head>-osiossa ilman attribuutteja, menetät hakusijoituksia ja ärsytät käyttäjiä. Tämä ei ole hypoteesi, Lighthouse ja PageSpeed Insights näyttävät ongelman punaisella raportin ensimmäisillä riveillä.

Nopea aloitus: käy läpi sivupohjasi <script>-tagit, lisää defer pääkoodille ja async metriikalle. Se vie viisi minuuttia, ja LCP voi parantua 300-500 ms. Seuraavaksi dynaaminen import() raskaille komponenteille, kun ehdit refaktoroida.

Jätä yksi lataustapa, <script defer> <head>-osioon, ja sivu latautuu ilman näkyviä viiveitä. Testaa sitä projektissasi tänään.