Rzadko można natrafić na stronę internetową, która nie używa skryptów JavaScript. W tym artykule omówimy techniki optymalizacji JavaScript pod kątem szybkości renderowania strony internetowej a także samej wydajności.

W artykule o zasobach blokujących renderowanie, wspominaliśmy o tym, że JavaScript wpływa na generowane drzewo renderowania. Oznacza to, że niezoptymalizowany JavaScript, może spowalniać stronę i znacząco pogarszać jakość doświadczeń użytkowników.

Okazuje się, że szukanie dziury w całym może prowadzić do doskonałości.

Jerzy Pilch

Wydzielenie skryptów JavaScript do osobnych plików

Wydzielenie skryptów do osobnych plików .js ma wiele zalet. Pojedyncza zmiana skryptu w jednym pliku powoduje przeniesienie zmian automatycznie do wszystkich stron na, których występuje plik. Zmniejsza rozmiar kodu HTML jaki jest przesyłany do przeglądarki.

Zapisanie skryptu do zewnętrznego pliku ma jedną wadę, aby przeglądarka mogła z niego skorzystać, musi wysłać ponowne żądanie HTTP i pobrać go jako zasób. Nie może przetwarzać kodu zanim plik .js nie zostanie w całości pobrany. Cały proces renderowania strony jest blokowany. W przypadku kiedy skrypt nie wpływa na CSSOM można wydzielić go do osobnego pliku i dodać atrybut async – wówczas nie blokuje renderowania.

async

Atrybut async mówi przeglądarce, że skrypt nie wpływa ani na DOM ani na CSSOM i może być przetworzony dopiero po załadowaniu i wyrenderowaniu całej podstrony. Atrybut async działa wyłącznie w przypadku skryptów umieszczonych w osobnym pliku – dlatego wydzielenie skryptu do osobnego pliku jest tak bardzo istotne.

Przykładem takich skryptów, które nie wpływają ani na DOM ani na CSSOM i spokojnie mogą być wydzielone do osobnych plików (lub pojedynczego pliku) z wykorzystaniem atrybutu async są skrypty narzędzi analitycznych. Skrypt jaki ma się znaleźć czy to w nagłówku czy w stopce można wkleić do zewnętrznego pliku a na stronie umieścić sam link.

<script src="scripts.js" async></script>

Należy pamiętać, że próba dołączenia tym sposobem skryptu JavaScript, który zmienia graficzną prezentację strony i manipuluje obiektami DOM może powodować nieprawidłowości np. zwiększenie się wskaźnika CLS. Nie należy też dodawać async do plików bibliotek, ponieważ zależne od nich skrypty mogą przestać działać. O tym jak pogodzić async i kolejność dołączania kodu zależnego opisano w dalszych punktach.

Blokujący JavaScript

Blokujący JavaScript to taki skrypt, który musi zostać wykonany przed zbudowaniem drzewa CSSOM. Skrypt JS blokuje renderowanie niezależnie czy jest on dołączony w postaci tagu z atrybutem src:

<script src="scripts.js"></script>

…czy jako skrypt inline.

Skrypt w kodzie HTML – inline

Skrypt w linii jest nieco lepszy pod kątem wydajności ponieważ nie wysyła dodatkowego zapytania HTTP – nie trzeba na niego czekać. Jednak także taki skrypt blokuje renderowanie, co więcej, nie można wykorzystać atrybutu async.

<script> function(){ /* jakiś skrypt*/ } </script>

defer

A co z defer? Atrybut defer działa dokładnie tak samo jak skrypt blokujący dodany przed znacznikiem zamykającym body. Jego działanie polega na tym, że nawet jeżeli tag <script> z atrybutem src jest dołączony do nagłówka, będzie się on działał tak jakby został dodany do stopki. Atrybut defer może spowodować, że skrypty umieszczone wyżej np. w tagu body zależne od skryptu, do którego dodano defer mogą przestać działać.

Programowanie asynchroniczne

Mając na uwadze powyższe informacje rodzi się ogromna pokusa dodania async do wszystkich skryptów a potem „poukładanie” kolejności ich wykonywania w taki sposób aby zależności nie zostały złamane, czy jest to możliwe? Tak, jednak wówczas wchodzimy w coś co nazwa się programowaniem asynchronicznym.

Jak pogodzić async i opóźnić skrypty zależne?

Wystarczy opóźnić wykonywanie się skryptów zależnych do momentu załadowania się bibliotek załadowanych asynchronicznie. Jedną z najprostszych technik jest użycie metody setTimeout(); Może ona być jednak zawodna a zasada działania łamie akademicką zasadę programowania asynchronicznego, która mówi, że: „Nie należy dokonywać żadnych przypuszczeń co do czasu wykonania się danego procesu„.

Zakładając, że ładujemy jQuery i dowolny skrypt od niego zależny zadziała nam taki mechanizm:

<script src="jquery.min.js" async></script>
<script>
setTimeout(function(){
   /* jakiś kod jQuery */
}, 5000);
</script>

Takie zakładanie, że jQuery będzie załadowany za 5 sekund jest bardzo nieprofesjonalne. Już lepiej wykonać coś takiego:

<script>
     var loadLoop = setInterval(function(){
          if(window.jQuery){
             clearInterval(loadLoop);
             /* Jakiś kod zależny od jQuery */
         }
 }, 100);
 </script> 

Ten problem da się jeszcze lepiej rozwiązać za pomocą własnego mechanizmu wykorzystującego wywołania zwrotne (ang. callbacks), promise, async/await lub z wykorzystaniem biblioteki require.js

Biblioteka Require.js

Biblioteka Require.js zoptymalizuje ładowanie potrzebnych bibliotek i jest wykorzystywana także w dużych aplikacjach. Potrafi ładować tylko potrzebne w danym momencie pliki i zachować zależności. Oto przykład w którym ładujemy jQuery i Bootstrap.

Do strony ładujemy tylko plik require.js z atrybutem async.

<script data-main="scripts/main" src="scripts/require.js" async></script>

W pliku scripts/main.js może znajdować się cały kod JavaScript z zależnościami. W tym przykładzie, dopiero po załadowaniu biblioteki jQuery i zależnej od niej biblioteki Bootstrap (i wczytaniu strony) jest wykonywany kod zależny.

requirejs.config({
     paths: {
         jquery: "lib/jquery",
         bootstrap: "lib/bootstrap.min"
     },
     shim: {
         bootstrap: {
             deps: ['jquery']
         }
     }
 });
 require(["jquery", "bootstrap"], function($) {
     jQuery(document).ready(function() {
         /* Jakiś kod zależny od jquery.js i bootstrap.js */
     });
 });

Optymalizacja plików JavaScript

Jak to zwykle bywa przy statycznych zasobach tekstowych – bo najczęściej takimi są pliki ze skryptami JavaScript – także w tym przypadku możemy wykorzystać poznane nam już techniki ich kompresowania i cache’owania.

Minifikacja (minimalizacja) skryptów JavaScript

Wszelkie zasoby tekstowe można minimalizować za pomocą usuwania nadmiarowych znaków takich jak spacje, tabulatory, znaki nowej linii czy nawet całych bloków komentarzy. Kod JavaScript można minimalizować w jeszcze większym stopniu niż kod HTML czy CSS. Mamy pod tym kątem większe możliwości z powodu istnienia wielu technik skracania składni. Nazwy funkcji i zmiennych można przekonwertować na jednoliterowe odpowiedniki, które nie zmienią sensu zaprogramowanych funkcjonalności. Czytaj więcej o minimalizacji zasobów tekstowych i technikach automatyzowania tego procesu.

Pamieć podręczna przeglądarki (Cache-Control, Expires, E-Tag)

Raz pobrany plik.js zawierający kod JavaScript może być zapisany w pamięci podręcznej przeglądarki – czyli najczęściej na lokalnym dysku. Przy następnej odsłonie podstrony, która odwołuje się do tego pliku nie będzie ona musiała pobierać tego pliku ponownie i wykonać kosztownego zapytania HTTP i pobierania.

Serwer może wysłać nagłówek Cache-Control lub Expires, który proponuje przeglądarce aby ta skopiowała plik i korzystała z niezmienionej wersji pliku przez określony czas. Czytaj więcej jak skorzystać z pamięci podręcznej przeglądarki w osobnym artykule: Pamięć podręczna HTTP.

Szybki serwer i CDN

Dobrą praktyką jest umieszczanie plików statycznych na osobnej subdomenie i integracja z siecią CDN. Różne serwery mają różny „czas reakcji” w serwowaniu statycznych plików czyli TTFB oraz różną przepustowość i jakość łącza internetowego. Choć są to małe pliki po kilkadziesiąt lub kilkaset kilobajtów, czuć różnicę kiedy ładujemy skrypty z szybkiego i poprawnie zoptymalizowanego serwera.

Kompresja

Pliki JavaScript są tekstem, a jak wiemy tekst można kompresować za pomocą standardowych algorytmów do kompresji danych. Kompresja wykonywana po stronie serwera za pomocą modułów np. Brotli, GZP lub deflate, jest niewidoczna dla użytkowników i pozwala zaoszczędzić aż do 80% transferu. Opracowany przez Google moduł Brotli, lub tradycyjne moduły deflate lub GZIP powinny być domyślnie zainstalowane i włączone na każdym serwerze, który serwuje pliki JavaScript a także inne zasoby tekstowe (CSS, XML).

Usuwanie nieużywanego kodu JavaScript

Czasem do stron dołączone są skrypty JavaScript, które nie pełnią żadnej funkcji. Mogą to być biblioteki, które zostały wykorzystane na innej podstronie lub funkcjonalności, które zostały wycofane w kolejnych aktualizacjach.

Skrypty JavaScript można poddać analizie Coverage – dokładnie tej samej, która służyła nam w przypadku usuwania nieużywanych instrukcji CSS.

Analiza Coverage JavaScript

Niewykorzystywane skrypty można całkowicie usunąć, lub wyciąć same bloki, które nie są wykorzystywane na żadnej podstronie.

Optymalizacja wydajności kodu JavaScript

Kod źródłowy programu można optymalizować pod kątem wydajności czyli ilości obliczeń jaką procesor musi wykonać. Działania te mogą nie tylko przyspieszać ładowanie strony ale oszczędzać energię w urządzeniach klienckich. W tym rozdziale zbiorę w punktach dobre praktyki i wskazówki jak można uniwersalnie optymalizować kod JavaScript.

Memoizacja

Memoizacja to uniwersalna technika optymalizacji kodu polegająca na zapisywaniu wyniku kosztownych funkcji lub odpowiedzi dla zadanych argumentów.

Cache zapytań do usług zewnętrznych

Możemy przechowywać w tymczasowej zmiennej zawartość pliku JSON lub XML jeżeli odwołujemy się do niego wielokrotnie a wiemy, że nie będzie ulegał częstym zmianom.

Throttling

Technikę throttlingu zaprezentowałem w przypadku programowania mechanizmu lazy-load. Polega na zmniejszeniu intensywności wyzwalania triggerów. Za pomocą prostych konstrukcji z instrukcjami warunkowymi i setTimeout można spowodować, że detektor zdarzeń (ang. event listener) zignoruje pewną część zdarzeń a funkcjonalność nadal będzie działać.

var eventTimeout;
var eventThrottler = function () {
if ( !eventTimeout ) {
eventTimeout = setTimeout(function() {
eventTimeout = null;
lazyLoad();
}, 1000);
}
};

Dzięki setTimeout sprawdzamy punkt wysokości co sekundę a nie ciągle.

Wykorzystywanie zmiennych lokalnych a nie globalnych

Wykorzystywanie zmiennych lokalnych jest efektywniejsze ponieważ zawężamy przestrzeń nazw. Wszystkie zmienne globalne są składnikiem obiektu window, w którym szukanie zmiennych może trwać stosunkowo długo.

Podsumowanie

Warto skorzystać z możliwości optymalizacji strony pod kątem JavaScript, ponieważ identyczne funkcjonalności wdrożone w różny sposób mogą znacznie spowalniać stronę albo w ogóle nie mieć wpływu na czas jej wczytywania. Mamy szeroki wachlarz możliwości, bo optymalizacji JavaScript można dokonać już w trakcie pisania skryptów, podczas dołączania ich do strony a także samego deploymentu.

Źródła

Oceń artykuł na temat: Optymalizacja skryptów JavaScript
Średnia : 4.7 , Maksymalnie : 5 , Głosów : 11