Bleskurychlé webové aplikace

Výpisek z mé přednášky na ČVUT:FIT 16.12. 2015 v Dejvicích. Na chyby mě prosím upozorněte v komentářích.

Jak vnímá uživatel rychlost aplikace

Rychlost Aplikace
  1. Aplikace je bolestivě pomalá, uživatel jí opustí
  2. Aplikace je snesitelná, uživatel používá
  3. Aplikace je plynulá, uživatel má radost

“Jakmile má uživatel opravdu radost, už se nevyplatí investovat do zrychlení.”

A nebo z druhé strany: 


“Investujte do zrychlení, dokud nemá uživatel opravdu radost.”


Prodleva
Reakce uživatele
0 - 100ms
Okamžitá reakce
100 - 300ms
Malá vnímatelná prodleva
300 - 1000ms
Stále v kontextu jednoho úkolu, vnímá prodlevu
1000+ ms
Mysl utíká k jinému problému (context switch)
10 000+ ms
Je to rozbité / přijdu později


60 FPS => 16ms (1/60)


RAIL Model

Model, který definuje časy pro svižnou aplikaci v různých typech interakce - k těmto hodnotám se pokoušíme přiblížit.
  • Reaction Time < 100ms Uživatel pociťuje okamžitou odezvu. Např. po kliknutí na tlačítko. Cokoliv delší a akce-reakce je rozbitá. 
  • Animation Time = 16ms Změny jsou plynulé. Např. scrollování. Jakákoliv změna plynulosti otravuje.
  • Idle Time “Na prázdno” = 50ms  Uživatel nic aktivně nedělá, takže aplikace má čas udělat práci. Musíme jí ale omezit na 50ms, aby aplikace mohla zareagovat na uživatele.
  • Load Time < 1s První zobrazení. Aby uživatel neztratil kontext, aby nezačal přemýšlet o něčem jiném. 


Bojujeme proti fyzikálním zákonům a rychlost světla je protivně pomalá



Žádnou informaci nejsme schopni přenést po povrchu země z mého bytu do Dejvic rychleji, než za 31.2ms. A to je ideální případ ve vakuu, kdy na cestě nejsou žádné jiné “zpomalovače”, jako switche a routery. Realita bude mnohem horší (např. mobil nejprve musí navázat připojení k věži operátora). 
Pokud 31.2ms porovnáme s čísly, které jsme si definovali pro RAIL, tak vidíme, že zde soupeříme s fyzikálními zákony a musíme k problému přistupovat chytře (trh a váš šéf většinou na fyzikální a matematické limity nedbá).

Poznatek: “Rychlost světla je protivně pomalá”



Jaké jsou rozdíly mezi nativními a webovými aplikacemi



Webové aplikace
Nativní aplikace (Android, iOS...)
Vždy aktualizované
Jednotný proces instalace (app store)
Fungují na všech platformách
Fungují offline
Jeden zdrojový kód
Rychlé + Přístup k GPU a periferiím
Nejsou blokovány pravidly app storů
Přístup k funkcím OS
Nejasný způsob instalace
Aktualizace nejsou pod kontrolou
Potřebují připojení k internetu
Každá platforma má odlišné schopnosti
Nemají přímý přístup k funkcím OS
Limitované pravidly app storů
Načítají se pomalu
Dražší na vývoj



Jak postupovat, když je aplikace pomalá?



  1. Měříme přesně a opakovatelně, jaké interakce v aplikaci trvají jak dlouho
  2. Analyzujeme, uděláme seznam problémů, seřadíme podle toho jak jsou pomalé a důležité
  3. Ze seznamu vezmeme první problém a vyřešíme/zlepšíme
  4. GOTO 1

Analýza: Jak funguje prohlížeč




Při hledání problému se zaměříme na tři hlavní situace, které mohou a často způsobují “pomalost”:
  1. Prohlížeč stáhne data stránky
  2. Prohlížeč vykreslí data na obrazovku
  3. Prohlížeč reaguje na pokyny uživatele

Jak funguje prohlížeč: Stažení dat potřebných pro zobrazení

Když prohlížeč stahuje data pro jQuery, tak pouze 1/10 času je stráveno samotným přenosem dat, zbytek je navazování šifrovaného připojení k serveru.

Každou sekci v tomto obrázku ukazujícím časovou osu pro stažení jQuery lze zkrátit, či úplně odebrat. A když ne pro první načtení, tak alespoň pro všechny další načtení.



Poznatek: “první odpověď od serveru (HTML) definuje řádek po řádku, jaké další soubory se mají načíst”


Jak funguje prohlížeč: Vykreslení na obrazovku



Parse HTML Krok po kroku přečte HTML kód a vytvoří DOM (Data Object Model).
JavaScript Zpracuje data a vynutí změnu, kterou je potřeba zobrazit na stránce.
Style Calculations Najde pro elementy jejich příslušně CSS styly (barvu, pozici, atd.).
Layout Spočítá, kam na stránce umístit všechny elementy a jak jsou veliké.
Paint Vykreslí jednotlivé pixely: fonty, barvy, obrázky, stíny.
Composite Sloučí vrstvy dohromady a vykreslí ve správném pořadí.


Jak funguje prohlížeč: Reakce na pokyny uživatele

  • JavaScript je jednovláknový. Když běží JavaScript, stránka je zaseklá a nedovolí žádné další interakce, ani např. scrollování.
  • Prohlížeč přepíná mezi dvěmi stavy - buď pouští JavaScript, nebo vykonává “svoje věci” (většinou napsané v C++), např. zobrazení elementů, reakce na scrollování a pod. viz předchozí sekce Vykreslení na obrazovku.
  • K tomu prohlížeč používa event-loop, nebo-li frontu s úkoly:
    1. Pokud je ve frontě nějaký úkol, prohlížeč (JavaScript engine) ho začne zpracovávat
    2. Když je jeden úkol v JS dokončen, tak prohlížeč přepne do stavu “svoje věci” a aktualizuje to co zobrazuje, vykreslí data, atd.
    3. Po malé chvilce zas prohlížeč zkontroluje, jestli je něco ve frontě a pokračuje bodem (a)
  • Body (a) až (c) se prohlížeč snaží vykonat do 16ms tak, aby stránka reagovala plynule.


(Zdroj: 2ality)

Jak na rychlou aplikaci

Je více různých možností a taktik, jak vytvořit rychlou aplikaci, ale my se v následujícím textu zaměříme na tyto 4 body:
  1. Progresivní načítání
    • Jak neblokovat prohlížeč
  2. Resource Hints
    • Přednačítání pomocí nových HTML APIs
  3. Web Worker
    • Provádění výpočtů ve Web Workers, neboli JS vícevláknově
  4. Service Worker
    • Uložení aplikace offline pomocí nového API pro psaní vlastní proxy v prohlížeči

Progresivní načítání


Nečekáme až se načtou veškeré zdroje (data, obrázky, JavaScript), ale zobrazujeme uživateli informace, jakmile dorazí. Podívejme se na následující vizualizaci:
(Zdroj a více informací: Google Developers: Critical rendering path)


Nic se ale nesmí přehánět - nechceme, aby uživateli obsah poskakoval na stránce a stránka blikala a měnila se pod ukazatelem. Proto je dobré rozdělit stránku na sekci, kterou uživatel uvidí hned po načtení (“above the fold”) a na sekci, ke které teprve musí doscrollovat.


(Zdroj: WordStream)

Toho docílíme tím, že rozdělíme zdroje (JS, data...) podle toho kdy by měly být načteny a řídíme kdy se data načítají.


Pravidlo: “Zdroje by měly být přednačteny co nejdříve a načteny co nejpozději”
Pokud toto pravidlo splníme, tak se
  • zdroje načtou velmi rychle
  • ale vytíží CPU, až když jsou potřeba

Jedním z přístupů je použít takzvaný “Application Shell”, tedy rozdělit aplikaci na data (html, layout, js), která se nikdy nemění (tzv. shell = ulita) a zbytek dat stáhnout až poté v zvláštním dotazu serveru:

Pro detaily doporučuji tento velmi pěkný článek: Getting Started with Progressive Web Apps, který vznikl až po mé původní prezentaci (inspiroval se Addy? :) ).
Pěkná ukázka takové aplikace je voice-memos.appspot.com (článek, zdrojáky).


Neblokujte prohlížeč v jeho práci

Jak jsme již zmínili, v první odpovědi posílá server prohlížeči index.html soubor. Tento soubor definuje další odkazy na soubory, které stránka potřebuje. Např.:

<!DOCTYPE html>
<html>
 <head>
   <script src="webcomponents-lite.min.js"></script>   ← Blokuje!
   <script src="app.js"></script>   ← Blokuje!
   <link rel="import" href="app-element.html">   ← Blokuje!
   <link rel="stylesheet" type="text/css" href="all-styles.css">   ← Blokuje!
 </head>
 <body>
   <app-element></app-element>
 </body>
</html>
Zjednodušeně: Prohlížeč zpracovává soubor index.html řádek po řádce a jednotlivé řádky mohou blokovat další zpracování, dokud odkazovaný soubor není stažen a spuštěn.




Pravidlo: “Žádný řádek v index.html by neměl blokovat další zpracování dotazem na server”

Následující kód ukazuje lepší způsob - po pár změnách již html neblokuje prohlížeč ani jedním řádkem:

<!DOCTYPE html>
<html>
 <head>
   <meta id="theme-color" name="theme-color" content="#263238"> ← Mobile App Theme
   <link rel="manifest" href="/manifest.json">    ← Offline Manifest
   <link rel="import" href="app-element.html" async>    ← Async Load
   <style>.main{height:64px;background:#263238 ...}</style>    ← Inline CSS
 </head>
 <body>
   <main class="main"></main>    ← Application Shell
   <aside class="toast-view"></aside>    ← Application Shell

   <app-element></app-element>

   <script async src="/app.js"></script>     ← Async Load
 </body>
</html>

Podívejme se např na “async” atribut u JavaScript souboru, který slouží k asynchronnímu (neblokujícímu) načtení:

<script> - standardní, blokuje parsování dalšího HTML<script async> - používejte kde to jde<script defer> - starší browsery

Jak doporučit prohlížeči co by měl přednačíst (Resource Hints)


Pokud víme, jakou akci uživatel typicky udělá hned poté co se stránka načte (např. ze stránky košíku jde uživatel na stránku objednávky), můžeme toho využít a říct prohlížeči, jaká bude další akce:

  • preconnect: DNS lookup, TCP handshake and TLS
<link rel="preconnect" href="https://example.com">
<link rel="preconnect" href="https://cdn.example.com" crossorigin>
    • další variant je rel=”dns-prefetch”, který pouze udělá DNS lookup

  • prefetch: přednačte zdroje
<link rel="prefetch" href="https://example.com/next-page.html" as="html" crossorigin="use-credentials">
<link rel="prefetch" href="/library.js" as="script">
    • nová lepší varianta je “preload”, který zruší request při navigaci

  • prerender: přednačte celé stránky
<link rel="prerender" href="//example.com/next-page.html">



Co když je JavaScript pomalý?


Co dělat, když naše výpočty v JavaScriptu trvají déle, než 16ms tak aby se vešly do jednoho snímku (60FPS)?
Naivní řešení je použít při výpočtu funkci setTimeout(fn, 10);, která zajistí že funkce fn bude prohlížečem zavolána až za 10ms a do té doby může prohlížeč provádět “svoje věci”.
Tato hodnota ale bohužel není synchronizována s tím, jak prohlížeč vykresluje snímky na obrazovku. Lehce se nám tak může stát následující situace, kdy zpracování JavaScriptu přesáhne do dalšího rámce (snímek se tím pádem nevykreslí, a stránka se chová “trhaně”):
Pravidlo: “Nepoužívejte setTimeout, nebo setInterval pro vizuální změny; vždy používejte requestAnimationFrame (a nově requestIdleCallback)”.
JavaScript, který musí bežet déle, než 16ms přesuňte do Web Worker.

Web Worker - JavaScript vícevláknově

Jednou z nových “API” definovaných pro HTML je Web Worker. Web Worker umožňuje běh JavaScriptu ve vlákně na pozadí a práce s ním je snadná:

// Zaregistrujeme nový WebWorker.
var dataSortWorker = new Worker("sort-worker.js");
// Pošleme workeru data na seřazení.
dataSortWorker.postMesssage(dataToSort);

// Hlavní vlákno může teď dělat libovolné akce.

// Ještě musíme poslouchat na odpověď od workeru.
dataSortWorker.addEventListener('message', function(evt) {
  var sortedData = evt.data;
  // Odpověď přišla, data jsou seřazená, můžeme zobrazit na stránce.
});

Model na posílání dat je pak velmi jednoduchý:

Jak na Offline

Service Worker je JS script, který běží na pozadí v prohlížeči odděleně od webové stránky. Umožňuje programátorovi napsat libovolný kód na zpracování všech dotazů mezi aplikací a serverem. 

Jedním z využití je pak způsob jak uložit aplikace v prohlížeči pro použití bez připojení k internetu.
(Zdroj a detailní možnosti: https://jakearchibald.com/2014/offline-cookbook/)


// Zaregistrujeme nový Service Worker
navigator.serviceWorker.register('/sw.js');

// Ve workeru (sw.js) načteme soubory
// a přidáme do ‘caches’  ...

// Ve workeru (sw.js) pak posloucháme
// na dotazy prohlížeče
self.addEventListener('fetch', function(event) {
 event.respondWith(
   caches.match(event.request).then(function(response) {
     return response || fetch(event.request);       // Worker si může dělat s dotazem co chce
   })
 );
});

Pro inspiraci se pak můžeme podívat na následující aplikace (zdrojové soubory jsou vždy k dispozici):


2 comments :

  1. I just got to this amazing site not long ago. I was actually captured with the piece of resources you have got here. Big thumbs up for making such wonderful blog page!
    Artificial Intelligence Course

    ReplyDelete