Výkonnost webových aplikací

Václav Chromický

O mně

O Bonami

Co dnes probereme?

  • Optimalizace na back-endu
    • Server
    • Infrastruktura
    • Architektura
  • Optimalizace na front-endu
    • Waterfall
    • Nástroje
    • React a Redux

Ale proooč?

Typický požadavek

GET /p/stastna-miska-bila HTTP/1.1
Host: www.bonami.cz

HAProxy

nginx

php-fpm

aplikace

Optimalizace back-endu

Optimalizace back-endu

Server

homer

Optimalizace na serveru

Nastavení serveru

OPCache, nastavení MySQL (velikosti bufferů apod.), konfigurace produkčního módu v aplikaci, ...

Optimalizace na serveru

ORM

public function calculateCashbackPrice(User $user): Price {
    $orderFilter = new OrderFilter();
    $orderFilter->setUser($user);
    $orderFilter->setCreatedAtBetween(/* ... */);

    $orders = $this->orderFacade->fetchList($orderFilter);
    $items = $this->orderItemFacade->getByOrders($orders);

    return $items->sumCustomerPrices();
}
public function calculateCashbackPrice(User $user): Price {
    return (
        $user->getOrders()
            ->filter(function (Order $order) {
                return $order->getCreatedAt()->isBetween(/* ... */);
            })
            ->getItems()
            ->sumCustomerPrice()
    );
}
O(1×1×m)
O(1×n×m)

Optimalizace na serveru

SQL

SELECT SUM(oi.customer_price_amount)
FROM order o
JOIN order_item oi ON oi.order_id = o.id
WHERE
    o.created_at BETWEEN /* ... */
    AND o.customer_id = ?;

Optimalizace na serveru

SQL

Optimalizace na serveru

Aplikační cache

  


public function calculateCashbackPrice(User $user): Price {
    /* ... */
}
/**
 * @FileCache(ttl=21600)
 */
public function calculateCashbackPrice(User $user): Price {
    /* ... */
}
/**
 * @RedisCache(ttl=600)
 */
public function getNavigation(Domain $domain): Navigation {
    /* ... */
}

Optimalizace na serveru

Rendering view

{% if product.availableAmount == 0 %}
<p>Vyprodáno.</p>
{% endif %}
{{ renderComponent(navigation)|raw }}

Optimalizace na serveru

Profiling (Blackfire)

Optimalizace na serveru

Shrnutí

  • Správně nakonfigurovaný server
  • ORM správně použito
  • Nativní SQL dotazy, kde potřeba
  • SQL dotazy optimalizované
  • Co je možné, je kešované
  • Rendering view je rychlý
  • Aplikace vyprofilovaná
     

Co když to nestačí?!

Optimalizace back-endu

Infrastruktura

Infrastruktura

Oddělení webového a databázového serveru

homer

marge

Infrastruktura

Read repliky databáze

homer

marge

marge_read1

marge_read2

marge_read3

Infrastruktura

Rozdělení dle uživatelů (tenants)

homer

marge

marge_cz

marge_pl

marge_sk

Infrastruktura

Oddělení služeb

homer

marge

apu

lisa

Infrastruktura

Thumbor

https://1.bonami.cz
    /images/products/be/4f/be4f53782c23ff12fc42124fc87072b7fa2f41e1-1000x1000.jpeg

Infrastruktura

Load balancing webového serveru

homer3

marge

apu

lisa

homer2

homer1

bumblebee

HAProxy

Infrastruktura

Shrnutí

  • Read repliky
  • Rozdělení po tenantech
  • Služby oddělené od webových serverů
  • Load balancing

Co když to nestačí?!

Optimalizace back-endu

Architektura aplikace

Architektura

Materializované pohledy

Předpočítávání dat

Architektura

Vygenerované dokumenty

{
    name: "Rohožka Cats, 40×60 cm",
    customerPrice: { 
        amount: 259,
        currency: "CZK",
    },
    images: { /* ... */ },
    rating: 0.93,
    ratingCount: 51,
    reviewCount: 14,
    reviews: { /* ... */ },
    /* ... */
}

Architektura

Vygenerované dokumenty

Architektura

Rozdělení aplikace

Architektura

CQRS

(Command Query Responsibility Segregation)

/api/product/63792/reserve?amount=4

Logika zápisu

Logika čtení

Architektura

CQRS - zápis

{
    command: "RESERVE_AMOUNT",
    productId: 63792,
    amount: 4
}
{
    event: "AMOUNT_RESERVED",
    productId: 63792,
    amount: 4
}

command handler

(queue)

!

/api/product/63792/reserve?amount=4

Architektura

CQRS - čtení

{
    event: "AMOUNT_RESERVED",
    productId: 63792,
    amount: 4
}

event listener

event listener

event listener

(notify)

web

http://dodavatel.cz/api/product/...

Architektura

Shrnutí

  • Materializované views
  • Předpočítaná data
  • Vygenerované dokumenty
  • Rozdělenou aplikaci na menší služby
  • Kritické části implementované v CQRS

Co když to nestačí?!

Bude muset!

Intermezzo

Continuous integration

Continuous integration

Continuous integration

quimby

leela

Optimalizace front-endu

Optimalizace front-endu

  • Waterfall
  • Nástroje
  • React a Redux

Waterfall

Waterfall

Optimalizace obrázků

Waterfall

Lazy loading

Waterfall

CSS sprity

Waterfall

Použití více subdomén

Waterfall

Minifikace CSS a JS

  .profile-order-overview {
    width: 100%;
  }

  .profile-order-overview tr td:last-child {
    display: none;
  }

  .profile-order-overview tr td {
    white-space: nowrap;
    padding: 3px;
  }

  .profile-order-overview tr td .meta-info {
    display: none;
  }

  #order-summary {
    width: 100%;
  }



  /* ... */
.profile-order-overview{width:100%}.profile-order-
overview tr td:last-child{display:none}.profile-or
der-overview tr td{white-space:nowrap;padding:3px}
.profile-order-overview tr td .meta-info{display:n
one}#order-summary{width:100%}}@keyframes fadeIn{f
rom{opacity:0}to{opacity:1}}@-webkit-keyframes fad
eIn{from{opacity:0}to{opacity:1}}@keyframes fadeOu
t{from{opacity:1}to{opacity:0}}@-webkit-keyframes 
fadeOut{from{opacity:1}to{opacity:0}}body{font:13p
x/1.5 "Segoe UI","Helvetica Neue",Helvetica,Arial,
sans-serif;background:#f5f5f5}@media(max-width:759
px){body{overflow-x:hidden}}.wrapper{width:100%;pa
dding:22px 0;background:#fff;margin:0 auto;positio
n:relative}@media(max-width:759px){.wrapper{paddin
g:11px 0}}.content{width:940px;padding:0 20px;min-
height:300px;margin:0 auto}.content.wide{width:100
%;padding:0;display:inline-block}@media(max-width:
759px){.content{width:300px;padding:0}.content.wid
e{width:100%}}h1{font-family:"bnmType-500",Helveti
ca,Arial,sans-serif;font-style:normal;-webkit-font
-smoothing:antialiased;font-size:40px;line-height:
54px;color:#1a1a1a;letter-spacing:-1px;margin:15px
 0 0 0}h2{font-family:"bnmType-300",Helvetica,Aria
/* ... */

CSS: 1029 KiB -> 844 KiB

JS: 2039 KiB -> 648 KiB

Waterfall

Použití komprese a http2

HTML: 173 KiB -> 107 KiB

CSS: 844 KiB -> 89 KiB

JS: 648 KiB -> 169 KiB

GET /p/stastna-miska-bila HTTP/1.1
Host: www.bonami.cz
Accept-Encoding: gzip, deflate

Waterfall

Konfigurace cache

GET /assets/styles.css HTTP/1.1
Host: 0.bonami.cz
If-Modified-Since: Wed, 15 Nov 2017 16:19:01 GMT
304 Not Modified
200 OK
Expires: Fri, 01 Jan 2100 00:00:00 GMT

200 OK
Last-Modified: Mon, 20 Nov 2017 07:28:00 GMT
GET /assets/tu078fm95u.css HTTP/1.1
Host: 0.bonami.cz

Waterfall

Umístění CSS a JS v HTML dokumentu

<html>
<head>
    <link href="//0.bonami.cz/assets/tu078fm95u.css" type="text/css" rel="stylesheet"/>
</head>
<body>
    <!-- ... -->
    <script src="//0.bonami.cz/assets/s0lk5cd9wg.js" async></script>
</body>
</html>

Waterfall

Rozdělení aplikace na více částí

Waterfall

Critical CSS

Waterfall

Rychlost CSS selektorů

.myClass {/* ... */}
.myContainer ul li {/* ... */}
.myContainer * {/* ... */}
.myClass .itsChild {/* ... */}
.myClass > .itsChild {/* ... */}
#myId {/* ... */}

Waterfall

Profiling (DevTools)

Optimalizace front-endu

Nástroje

Nástroje

Chrome DevTools

Nástroje

WebPagetest.org

Nástroje

Lighthouse

Nástroje

PageSpeed Insights

Nástroje

Shrnutí

Pracujte s nástroji! :-)

Úzká hrdla v Reactu a Reduxu

React.PureComponent

Volání connect()

Úzká hrdla v Reactu a Reduxu

React.PureComponent

Nebuďte lamy :-).

export class MojeKomponenta extends React.Component<StateProps & DispatchProps, State> {

    public shouldComponentUpdate(nextProps): bool {
        return true;
    }

    /* ... */
}
export class MojeKomponenta extends React.PureComponent<StateProps & DispatchProps, State> {

    /* ... */
}
{
    draft: {
        rows: [{
                name: "Krémové křeslo Stella",
                url: "kreslo-stella-cream",
                images: { ... },
                /* ... */
            }, {
                /* Ještě 9999× ... */
            }
        ]
    }
}

Úzká hrdla v Reactu a Reduxu

Volání connect()

Úzká hrdla v Reactu a Reduxu

Volání connect()

class Table extends React.PureComponent<...> {

    public render() {
        return this.props.row.map(() =>
            <Row ... />
        );
    }
}

export default connect(...)(MyTable);
class Row extends React.PureComponent<...> {

    public render() {
        /* ... */
    }
}



export default connect(...)(Row);

connect()

shouldComponentUpdate()

Úzká hrdla v Reactu a Reduxu

Volání connect()

public render() {
    const {sliceStartRow, sliceEndRow} = this.state;
    const slicedRows = this.props.rows.slice(sliceStartRow, sliceEndRow);
    return slicedRows.map((row, i) => <Row data={row} key={sliceStartRow + i} />);
}
    

Úzká hrdla v Reactu a Reduxu

Shrnutí

  • Hlídejte počet volání connect()
  • Hlídejte počet volání render()
  • Hlídejte počet volání shouldComponentUpdate()
  • Stokrát nic osla umořilo

Pro koho to děláme?

Optimalizace front-endu

Lidská psychika

  • Big O je váš kámoš.
  • Pomalý web/aplikace většinou nepůsobí na člověka dobře
    a snižuje konverze.
  • Jako dobrý back-enďák tuší, co se děje na front-endu...
  • ...dobrý front-enďák tuší, co se děje na back-endu.
  • Nejde jen o to implementovat render() a reduce().

Závěrem

Tak dík! :-)

Dotazy?

vaclav.chromicky@bonami.cz

Výkonnost webových aplikací (VŠE) 2018

By vasekch

Výkonnost webových aplikací (VŠE) 2018

  • 269
Loading comments...

More from vasekch