
⚡ 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- jadefer-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:
1 function 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 |
|---|---|---|---|
| Rinnakkain jäsentämisen kanssa | Välittömästi latauksen jälkeen | Ei taattu |
| Rinnakkain jäsentämisen kanssa | Täyden HTML-jäsentämisen jälkeen | Taattu (kuten dokumentissa) |
Nyrkkisääntö:
asyncriippumattomille skripteille: analytiikka, mainokset, laskurit. Ne eivät tarvitse DOM-puuta, eivätkä välitä järjestyksestä.deferpää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 2 button.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 |
|---|---|---|---|---|
| Kyllä | Ei | Taattu | Ei käytetä ellei ole pakko |
Dynaaminen | Ei | Kyllä |
| Ehdollinen lataus, raskaat riippuvuudet |
| Ei | Ei | Ei taattu | Analytiikka, mainokset, laskurit |
| Ei | Ei | Taattu | Pääsovellus, DOM-manipulaatio |
| Ei | Kyllä (ES-moduuli) |
| 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
asyncsuorittaa skriptin heti tiedoston latauduttua, vaikka HTML ei olisi vielä täysin jäsennetty, eikä takaa järjestystä.deferodottaa aina täyttä DOM-valmiutta ja säilyttää skriptien järjestyksen kuten HTML:ssä. Pääsovelluskoodille käytädefer-attribuuttia, eristetyille laskureilleasync-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 dynaamisellaloadScript()- taiimport()-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- taiasync-attribuutinwp_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 onasync-attribuutti. Monimutkaiset interaktiiviset lohkot (laskurit, suodattimet) kannattaa siirtää dynaamiseenimport()-kutsuun mukautetun moduulin sisällä.
Toimiiko tämä kolmannen osapuolen skriptien, kuten Google Analyticsin, kanssa?
Kyllä. GA4-tagi
gtag.jslatautuu oletuksenaasync-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- taiasync-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.



