Transmisja w Internecie przyspiesza z dnia na dzień. Jeszcze 15 lat temu na załadowanie prostej strony Interii z grą flash czekałem około minuty. Obecnie duże aplikacje internetowe obsługują setki tysięcy klientów w każdej sekundzie. Każde z żądań musi być obsłużone w przeciągu kilku sekund. Każde przeciążenie serwera jest potencjalną stratą dla właściciela aplikacji.

Drogi optymalizacji

Mam aplikację, która cieszy się popularnością. Obecnie nie stać mnie na skalowanie w którąkolwiek stronę, a użytkownicy zaczynają dostawać błąd HTTP 504 lub strona ładuje się potwornie długo. Co zatem mogę poprawić?

Istnieją dwie drogi warte sprawdzenia, a mianowicie:

  • zmniejszenie objętości komunikatów,
  • zmniejszenie liczby komunikatów.

Do zmniejszenia objętości komunikatów mogę wykorzystać mechanizm minifikacji (kompresji skryptów i styli), a także kompresję HTTP. W celu zmniejszenia liczby żądań do mojego serwera użyję bundlera i serwerów CDN.

Bundler

Na stronie głównej aplikacji mam dołączonych kilkadziesiąt plików CSS i JavaScript. Co się dzieje podczas ładowania strony? Przeglądarka dla każdego pliku wykonuje osobne żądanie HTTP! Jeżeli klient ma włączone cachowanie, zawartość plików zostanie załadowana jedynie raz. Przy kolejnych odwiedzinach serwer będzie zwracał HTTP 304. W ten sposób powstają dwa problemy:

  1. serwer jest niepotrzebnie obciążany dodatkowymi zapytaniami,
  2. klient wykonuje zbędne zapytania, co wydłuża czas ładowania strony.

Problem można częściowo rozwiązać za pomocą bundlera. Bundler jest narzędziem łączącym kilka plików w jeden. ASP.NET używa do tego celu biblioteki Microsoft ASP.NET Web Optimization Framework. Wdrożenie “paczkowania” jest bardzo proste.

Chcę połączyć zawartość styli załączonych za pomocą kodu:

<link href="/Content/bootstrap.css" rel="stylesheet">
<link href="/Content/site.css" rel="stylesheet">

W pierwszym kroku wybieram pliki, które chcę scalić. Dodaję wpis do pliku App_Start\BundleConfig.cs (na końcu funkcji RegisterBundles()):

bundles.Add(new StyleBundle("~/Content/css").Include(
    "~/Content/bootstrap.css",
    "~/Content/site.css"));

W drugim kroku możemy już skorzystać z bundla. W treści mojego widoku zastępuję stary kod HTML za pomocą kodu:

@Styles.Render("~/Content/css")

W wyniku tej operacji ASP.NET załącza do dokumentu jeden styl CSS, który jest sklejeniem dwóch wymienionych plików:

<link href="/Content/css?v=Bz3KZjU_pdOm2wAVr7z_ylCuQzQDs1O8N6pV4cvXc_Q1" rel="stylesheet">

Minifikacja

Otwierając otrzymany plik widzę nietypową zawartość:

article,aside,details,figcaption,figure,footer,header,hgroup,main,nav,section,summary{display:block}audio (...)

Powstała paczka jest pozbawiona zbędnych białych znaków i komentarzy - jest zminifikowana. Spakowanie danych w ten sposób pozwala na zmniejszenie wielkości pliku nawet o kilkadziesiąt procent. W przypadku plików JavaScript minifikacja idzie o krok dalej. Skraca nazwy lokalnych funkcji i zmiennnych. W związku z tym plik jest jeszcze mniejszy, co oszczędza przepustowość serwera. Zminifikowane wersje skryptów i styli zazwyczaj są dostarczane wraz z oryginalnym kodem źródłowym w paczce NuGet. Jeżeli tak nie jest, możesz użyć narzędzia do minifikacji, np. Web Essentials. Istotne jest, aby zminifikowany skrypt/styl miał taką samą nazwę jak oryginał, a zmieniony format na .min.js/.min.cs (np. script.js -> script.min.js).

Serwery CDN

Serwery CDN są publicznymi repozytoriami popularnych skryptów i arkuszy stylów (np. bootstrap, jquery, angular.js). Utrzymywane są przez dużych dostawców usług internetowych, np. Google. Korzystanie z nich rozwiązuje problem ograniczenia liczby połączeń, a także ułatwia korzystanie z cache przeglądarku.

Ograniczenie liczby połączeń w przeglądarkach WWW

Przeglądarki internetowe mają domyślnie ograniczoną liczbę połączeń z danym serwerem. Jeżeli moja aplikacja będzie miała zbyt wiele zależności w osobnych plikach, nie wszystkie będą pobierane równolegle. Użycie serwera CDN zmniejszy licznik równoległych połączeń dla serwera mojej aplikacji.

Internet Explorer 7 i kilka innych przeglądarek ograniczają liczbę połączeń na serwer do dwóch. W przypadku pisania aplikacji internetowej dla starszych przeglądarek użycie CDNów może dać istotne przyspieszenie ładowania strony.

Cache

Popularne skrypty/style z serwerów CDN prawdopodobnie znajdują się już w cache przeglądarki użytkownika. Zysk jest oczywisty - klient nie musi ponownie pobierać skryptu/stylu.

Popularne serwery CDN

Serwery z których korzystam to:

Fallback, czyli co zrobić, gdy serwer CDN nie odpowiada

Czasem zdarza się sytuacja, w której klient nie ma połączenia z serwerem CDN. W takiej sytuacji moja strona załaduje się bez niezbędnych skryptów. Rozwiązaniem tego problemu jest tzw. fallback, czyli załączenie biblioteki z mojego serwera w przypadku awarii CDN. Implementacja w HTML/JS jest niezwykle prosta:

<script src="//ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<script>
if(!window.jQuery){
    document.write('<script src="/path/to/your/jquery"></script>');}
</script>

Oczywiście przy takim podejściu tracimy możliwość globalnej zmiany załączonego skryptu w BundleConfig.cs. Na szczęście ASP.NET wspiera fallback:

bundles.UseCdn = true;
var jquery = new ScriptBundle("~/bundles/jquery", "//ajax.aspnetcdn.com/ajax/jquery/jquery-2.1.1.min.js")
    .Include("~/Scripts/jquery-{version}.js");
jquery.CdnFallbackExpression = "window.jQuery";
bundles.Add(jquery);

ASP.NET obsługuje również fallback dla arkuszy CSS, jednak rozwiązanie problemu ze stylami jest nieco trudniejsze. Więcej informacji znajdziesz tutaj.

Bundler vs CDN

Co jest bardziej opłacalne: połączenie kilku skryptów w jeden i dołączenie do dokumentu HTML, czy załączenie każdego osobno za pośrednictwem CDNów? Jeżeli masz możliwość, użyj serwerów CDN. Bundler jest świetnym rozwiązaniem w przypadku własnych lub mało popularnych skryptów.

Kompresja HTTP (IIS)

Serwer IIS ma możliwość kompresowania treści komunikatów osobno dla plików statycznych i generowanych dynamicznie. Istnieje 10 poziomów kompresji. Każdy kolejny daje większy stopień kompresji przy jednoczesnym większym zużyciu czasu procesora. Co więcej, IIS ma możliwość wyłączenia kompresji przy dużym obciążeniu CPU. Powołując się na ten test, ustaliłbym poziom kompresji następująco:

  • pliki statyczne: 7-9, w zależności od częstości używania plików (pliki statyczne są cachowane),
  • pliki dynamiczne 0-4, w zależności od klientów (np. klienci mobilni mają ograniczone możliwości przesyłania danych przez sieć komórkową).

Pimp my bundler

Poniżej znajdziecie ściągę dla popularnych skryptów, wraz z fallbackami:

// jQuery
var jquery = new ScriptBundle("~/bundles/jquery", "//ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js")
{
    CdnFallbackExpression = "window.jquery"
};
jquery.Include("~/Scripts/jquery-{version}.js");
bundles.Add(jquery);

// jQuery UI
var jqueryUi = new ScriptBundle("~/bundles/jquery", "//ajax.googleapis.com/ajax/libs/jqueryui/1.10.4/jquery-ui.min.js")
{
    CdnFallbackExpression = "window.jquery.ui"
};
jqueryUi.Include("~/Scripts/jquery-{version}.js");
bundles.Add(jqueryUi);

// Bootstrap JS
var bootstrap = new ScriptBundle("~/bundles/bootstrap", "//netdna.bootstrapcdn.com/bootstrap/3.1.1/js/bootstrap.min.js")
{
    CdnFallbackExpression = "$.fn.modal"
};
bootstrap.Include("~/Scripts/bootstrap.js");
bundles.Add(bootstrap);

// Respond.js
var respondjs = new ScriptBundle("~/bundles/respondjs", "//cdnjs.cloudflare.com/ajax/libs/respond.js/1.4.2/respond.js")
{
    CdnFallbackExpression = "window.respond"
};
respondjs.Include("~/Scripts/respond.js")
bundles.Add(respondjs);

// Moment.js
var momentjs = new ScriptBundle("~/bundles/momentjs","//cdnjs.cloudflare.com/ajax/libs/moment.js/2.6.0/moment.min.js")
{
    CdnFallbackExpression = "window.moment"
};
momentjs.Include("~/Scripts/moment.js");
bundles.Add(momentjs);