- Регистрация
- Сообщения
- 736
- Реакции
- 36
Клик отрабатывает моментально, при этом мы видим что запрос на backend отработал только через 2 секунды.
Как реализовать Optimistic Updates у себя? Давайте рассмотрим самый простой пример http запроса:
addItem(item) {
return this.http.post<Item>('/add-item', item)
.subscribe(res => {
this.items.push(res);
}, error => {
this.showErrorMessage(error);
})
}
Теперь применим к нему Optimistic Update:
addItem(item) {
this.items.push(item); // делаем заранее
return this.http.post<Item>('/add-item', item)
.subscribe(
res => {
// ничего не делаем в случае успеха
}, error => {
// в случае ошибки - откатываем состояние
this.items.splice(this.items.indexOf(item), 1);
this.showErrorMessage(error);
}
);
}
Для State Managers все немного сложнее, но для каждого есть своя реализация и свои паттерны для реализации Optimistic Updates. В этой статье я не буду приводить примеры, их легко можно найти под подходящий State Manager (redux, mobx, appollo-graphql, etc.)
addItem(item) {
return this.http.post<Item>('/add-item', item)
.subscribe(res => {
this.items.push(res);
}, error => {
this.showErrorMessage(error);
})
}
Мы можем сделать так, что API будет выглядеть более стабильным за счет одной простой функции. Получив ошибку на запрос, просто повторяем его.
addItem(item) {
this.items.push(item); // делаем заранее
return this.http.post<Item>('/add-item', item)
.pipe(retry()) // повторяем запрос
.subscribe(
// ...
);
}
Это одна из best practice в энтерпрайз-приложениях, потому что бэкенд может не работать по разным причинам. Например, происходит деплой или хитрая маршрутизация. В любом случае очень хорошее решение: попробовать повторить. Ведь никакое API не дает 100% гарантии, 99,9% uptime.
Но есть маленький нюанс. Если мы будем повторять бесконечно, то в какой-то момент повалим наш сервер. Поэтому обязательно поставьте ограничение. Например, повторять максимум 3 раза.
addItem(item) {
this.items.push(item); // делаем заранее
return this.http.post<Item>('/add-item', item)
.pipe(
retry(3)
)
.subscribe(
// ...
);
}
Но даже с этим сценарием мы сами себе можем сделать DDOS (Distributed Denial of Service). На это попадались многие компании. Например, Apple с запуском своего сервиса MobileMe.
В чем идея? Представьте на секунду, что ваш сервис падает под нагрузкой, то есть он с ней не справляется. Если сервер перегружен, то скорее всего ответит статус-кодом 500. Если вы попробуете еще раз, возможно, следующий запрос получит ответ.
Но если сервер упал под нагрузкой, и у всех одновременно запросы ответили с ошибкой, то все начнут перезапрашивать в одно и то же время, вы сами устроите себе DDOS. На сервер будет идти постоянная нагрузка, он не сможет передохнуть и наконец-то начать отвечать нормально.
Best practice: применять exponential backoff. В rxjs есть хороший дополнительный npm пакет backoff-rxjs, который за вас имплементирует данный паттерн.
import { retryBackoff } from 'backoff-rxjs';
addItem(item) {
this.items.push(item);
return this.http.post<Item>('/add-item', item)
.pipe(
retryBackoff({
initialInterval: 100,
maxRetries: 3,
resetOnSuccess: true
})
)
.subscribe(
// ...
);
}
Имплементация очень простая, 10 строчек кода. Здесь вы можете обойтись одной. Указываете интервал, через который начнутся повторы, количество попыток, и сбрасывать ли увеличивающийся таймер. То есть вы увеличиваете по экспоненте каждую следующую попытку: первую делаете через 1 с, следующую через 2 с, потом через 4 с и т.д.
Играя с этими настройками, вы можете настраивать их под ваше API.
Следующий совет очень простой — добавить Math.random() для initialInterval:
import { retryBackoff } from 'backoff-rxjs';
addItem(item) {
this.items.push(item);
return this.http.post<Item>('/add-item', item)
.pipe(
retryBackoff({
initialInterval: Math.random() * 100,
maxRetries: 3,
resetOnSuccess: true
})
)
.subscribe(
// ...
);
}
Идея в том, что изначально, если все одновременно начнут повторять запросы, все равно может возникнуть ситуация, когда появится огромное количество запросов одномоментно. Поэтому добавив некий рандомный запрос, вы как бы размажете нагрузку повторных запросов на бэкенд.
Вы можете просто делать все заранее. К примеру, если вы занимаетесь изготовлением мороженого и знаете, что оно готовится очень долго, можете сделать его заранее, поставить в холодильник и избежать огромной очереди. Этот прием можно применить в сценариях с веб-производительностью.
var images = [];
function preload() {
for (var i = 0; i < arguments.length; i++) {
images = new Image();
images.src = preload.arguments;
}
}
preload(
"http://domain.tld/image-001.jpg",
"http://domain.tld/image-002.jpg",
"http://domain.tld/image-003.jpg"
)
Этот способ слишком «олдскульный», потому что есть предзагрузка для взрослых.
<link
rel="stylesheet"
href="styles/main.css"
>
Немного поменяв атрибуты, мы можем применить его для предзагрузки:
<link
rel="preload"
href="styles/main.css"
as="style"
>
Можно указать атрибут rel ="preload", сослаться в ссылке на наш элемент (href="styles/main.css"), и в атрибуте as описать тип предзагружаемого контента.
<link
rel="prefetch"
href="styles/main.css"
>
Главное — запомнить, что preload и prefetch — два самых полезных инструмента. Отличие preload от prefetch в том, что preload заставляет браузер делать запрос, принуждает его. Обычно это имеет смысл, если вы предзагружаете ресурс на текущей странице, к примеру, hero images (большую картинку).
ОК, это уже лучше, но есть одна маленькая проблема.
Если взять какой-нибудь среднестатистический сайт и начать префетчить все JavaScript модули, то средний рост по больнице составляет 3 МБ. Если мы будем префетчить только то, что видим на странице, получаем примерно половину — 1,2 МБ. Ситуация получше, но все равно не очень.
Что же делать?
Анализируя паттерны поведения пользователей и, подключаясь к вашему приложению и системе сборки, она может интеллектуально делать prefetch только 7% на странице.
При этом эта библиотека будет точна на 90%. Загрузив всего 7%, она угадает желания 90% пользователей. В результате вы выигрываете и от prefetching/preloading, и от того, что не загружаете все подряд. Guess.js — это идеальный баланс.
Сейчас Guess.js работает из коробки с Angular, Nuxt js, Next.js и Gatsby. Подключение очень легкое.
<div (сlick)="onClick()">
Это button! Привет Вадим
</div>
Как предугадать, на что кликнет пользователь? Есть очевидный ответ.
У нас есть событие, которое называется mousedown. Оно срабатывает в среднем на 100-200 мс раньше, чем событие Click.
Применяется очень просто:
<div (onmousedown)="onClick()">
Это button! Привет Вадим
</div>
Просто поменяв click на mousedown, мы можем выиграть 100-200 мс.
Я пытаюсь кликнуть на ссылку, и сайт мне говорит, что он знал об этом на 240-500 мс раньше того, как я это сделал .
Опять магия ML? Нет. Существует паттерн: когда мы хотим на что-то кликнуть, мы замедляем движение (чтобы было легче попасть мышкой на нужный элемент).
Есть библиотека, которая анализирует с какой скоростью замедляется движение мыши, и благодаря этому может предсказать, куда я кликну.
Библиотека называется futurelink. Ее можно использовать абсолютно с любым фреймворком:
var futurelink = require('futurelink');
futurelink({
links: document.querySelectorAll('a'),
future: function (link) {
// `link` is probably going to be clicked soon
// Preload everything you can!
}
});
Вы передаете ей те DOM элементы, которые должны участвовать в прогнозе. Как только библиотека предсказывает нажатие на элемент, сработает callback. Вы можете его обработать и начать загружать страницу или залогинить пользователя заранее.
Что пользователь хочет получить при переходе на страницу? В большинстве сценариев: HTML, CSS и немного картинок.
Все это можно реализовать за счет серверного рендеринга SSR.
В Angular для этого достаточно добавить одну команду:
ng add @nguniversal/express-engine
В большинстве случаев это работает замечательно, и у вас появится Server-Side Rendering.
Но что, если вы не на Angular? Или у вас большой проект, и вы понимаете, что внедрение серверного рендеринга потребует довольно большого количества усилий?
Здесь можно воспользоваться статическим prerender: отрендерить страницы заранее, превратить их в HTML. Для этого есть классный плагин для webpack, который называется PrerenderSPAPlugin:
const path = require('path')
const PrerenderSPAPlugin = require('prerender-spa-plugin')
module.exports = {
plugins: [
...
new PrerenderSPAPlugin({
staticDir: path.join(__dirname, 'dist'),
routes: [ '/', '/about', '/some/deep/nested/route' ],
})
]
}
В нем нужно указываете директорию, куда вы хотите сохранять пререндеренные urls, и перечислить те, для которых нужно отрендерить контент заранее.
Но вы можете сделать все еще проще: зайти в свое SPA приложение и написать:
document.documentElement.outerHTML
получить пререндеренный HTML и воспользоваться им: сохранить это в файл. И вот у вас за пару минут появилась пререндеренная страница. Если ваша страница меняется очень редко, это неплохой вариант (как бы глупо он ни выглядел).
Он рассказывал о том, что в Скелетоне есть анимация в плавном фоне, и слева направо она воспринимается на 68% быстрее, чем справа налево. Есть разные исследования, которые показывают, что неправильно примененные техники Perceived Performance могут визуально сделать сайт хуже. Поэтому обязательно тестируйте.
Сделать это можно при помощи сервиса под названием Яндекс.Толока.
Он позволяет выбирать, какой из двух объектов лучше. Можно сделать два видео с разной анимацией и предложить пользователям оценить, какой из них быстрее. Это стоит недорого, скейлится очень хорошо. В сервисе зарегистрировано огромное количество людей из разных регионов, то есть можно делать разнообразные выборки по разным параметрам. Благодаря этому есть возможность провести серьезное UX-исследование за небольшую сумму.
Даже если в конце концов вашему начальнику покажется, что быстрее не стало, проведите исследование и попробуйте улучшить ситуацию с помощью Perceived Performance.
Perceived performance — это низковисящий фрукт почти в любой компании. Именно сейчас у вас есть возможность, применив вышеперечисленные техники, улучшить воспринимаемую производительность и сделать пользователей и менеджеров счастливыми.
Подписывайтесь на Telegram канал obenjiro_notes и Twitter obenjiro.
Как реализовать Optimistic Updates у себя? Давайте рассмотрим самый простой пример http запроса:
addItem(item) {
return this.http.post<Item>('/add-item', item)
.subscribe(res => {
this.items.push(res);
}, error => {
this.showErrorMessage(error);
})
}
Теперь применим к нему Optimistic Update:
addItem(item) {
this.items.push(item); // делаем заранее
return this.http.post<Item>('/add-item', item)
.subscribe(
res => {
// ничего не делаем в случае успеха
}, error => {
// в случае ошибки - откатываем состояние
this.items.splice(this.items.indexOf(item), 1);
this.showErrorMessage(error);
}
);
}
Для State Managers все немного сложнее, но для каждого есть своя реализация и свои паттерны для реализации Optimistic Updates. В этой статье я не буду приводить примеры, их легко можно найти под подходящий State Manager (redux, mobx, appollo-graphql, etc.)
Экспоненциальная выдержка
Следующее, что необходимо проявлять — это не обычную выдержку, а экспоненциальную. Optimistic Updates имеют одну проблему: мы обязаны откатить состояния приложения назад в случае наличия проблемы с запросом и показать сообщение об ошибке. Но эту ситуацию можно сгладить. Давайте посмотрим на простой пример с Optimistic Update:addItem(item) {
return this.http.post<Item>('/add-item', item)
.subscribe(res => {
this.items.push(res);
}, error => {
this.showErrorMessage(error);
})
}
Мы можем сделать так, что API будет выглядеть более стабильным за счет одной простой функции. Получив ошибку на запрос, просто повторяем его.
addItem(item) {
this.items.push(item); // делаем заранее
return this.http.post<Item>('/add-item', item)
.pipe(retry()) // повторяем запрос
.subscribe(
// ...
);
}
Это одна из best practice в энтерпрайз-приложениях, потому что бэкенд может не работать по разным причинам. Например, происходит деплой или хитрая маршрутизация. В любом случае очень хорошее решение: попробовать повторить. Ведь никакое API не дает 100% гарантии, 99,9% uptime.
Но есть маленький нюанс. Если мы будем повторять бесконечно, то в какой-то момент повалим наш сервер. Поэтому обязательно поставьте ограничение. Например, повторять максимум 3 раза.
addItem(item) {
this.items.push(item); // делаем заранее
return this.http.post<Item>('/add-item', item)
.pipe(
retry(3)
)
.subscribe(
// ...
);
}
Но даже с этим сценарием мы сами себе можем сделать DDOS (Distributed Denial of Service). На это попадались многие компании. Например, Apple с запуском своего сервиса MobileMe.
В чем идея? Представьте на секунду, что ваш сервис падает под нагрузкой, то есть он с ней не справляется. Если сервер перегружен, то скорее всего ответит статус-кодом 500. Если вы попробуете еще раз, возможно, следующий запрос получит ответ.
Но если сервер упал под нагрузкой, и у всех одновременно запросы ответили с ошибкой, то все начнут перезапрашивать в одно и то же время, вы сами устроите себе DDOS. На сервер будет идти постоянная нагрузка, он не сможет передохнуть и наконец-то начать отвечать нормально.
Best practice: применять exponential backoff. В rxjs есть хороший дополнительный npm пакет backoff-rxjs, который за вас имплементирует данный паттерн.
import { retryBackoff } from 'backoff-rxjs';
addItem(item) {
this.items.push(item);
return this.http.post<Item>('/add-item', item)
.pipe(
retryBackoff({
initialInterval: 100,
maxRetries: 3,
resetOnSuccess: true
})
)
.subscribe(
// ...
);
}
Имплементация очень простая, 10 строчек кода. Здесь вы можете обойтись одной. Указываете интервал, через который начнутся повторы, количество попыток, и сбрасывать ли увеличивающийся таймер. То есть вы увеличиваете по экспоненте каждую следующую попытку: первую делаете через 1 с, следующую через 2 с, потом через 4 с и т.д.
Играя с этими настройками, вы можете настраивать их под ваше API.
Следующий совет очень простой — добавить Math.random() для initialInterval:
import { retryBackoff } from 'backoff-rxjs';
addItem(item) {
this.items.push(item);
return this.http.post<Item>('/add-item', item)
.pipe(
retryBackoff({
initialInterval: Math.random() * 100,
maxRetries: 3,
resetOnSuccess: true
})
)
.subscribe(
// ...
);
}
Идея в том, что изначально, если все одновременно начнут повторять запросы, все равно может возникнуть ситуация, когда появится огромное количество запросов одномоментно. Поэтому добавив некий рандомный запрос, вы как бы размажете нагрузку повторных запросов на бэкенд.
Предугадывайте!
Как уменьшить ожидание, когда невозможно ускорить процесс?Вы можете просто делать все заранее. К примеру, если вы занимаетесь изготовлением мороженого и знаете, что оно готовится очень долго, можете сделать его заранее, поставить в холодильник и избежать огромной очереди. Этот прием можно применить в сценариях с веб-производительностью.
- Предзагрузка картинок;
var images = [];
function preload() {
for (var i = 0; i < arguments.length; i++) {
images = new Image();
images.src = preload.arguments;
}
}
preload(
"http://domain.tld/image-001.jpg",
"http://domain.tld/image-002.jpg",
"http://domain.tld/image-003.jpg"
)
Этот способ слишком «олдскульный», потому что есть предзагрузка для взрослых.
- Предзагрузка 18+
<link
rel="stylesheet"
href="styles/main.css"
>
Немного поменяв атрибуты, мы можем применить его для предзагрузки:
<link
rel="preload"
href="styles/main.css"
as="style"
>
Можно указать атрибут rel ="preload", сослаться в ссылке на наш элемент (href="styles/main.css"), и в атрибуте as описать тип предзагружаемого контента.
- prefetch.
<link
rel="prefetch"
href="styles/main.css"
>
Главное — запомнить, что preload и prefetch — два самых полезных инструмента. Отличие preload от prefetch в том, что preload заставляет браузер делать запрос, принуждает его. Обычно это имеет смысл, если вы предзагружаете ресурс на текущей странице, к примеру, hero images (большую картинку).
ОК, это уже лучше, но есть одна маленькая проблема.
Если взять какой-нибудь среднестатистический сайт и начать префетчить все JavaScript модули, то средний рост по больнице составляет 3 МБ. Если мы будем префетчить только то, что видим на странице, получаем примерно половину — 1,2 МБ. Ситуация получше, но все равно не очень.
Что же делать?
Давайте добавим Machine Learning
Сделать это можно с помощью библиотеки Guess.js. Она создана разработчиками Google и интегрирована с Google Analytics.Анализируя паттерны поведения пользователей и, подключаясь к вашему приложению и системе сборки, она может интеллектуально делать prefetch только 7% на странице.
При этом эта библиотека будет точна на 90%. Загрузив всего 7%, она угадает желания 90% пользователей. В результате вы выигрываете и от prefetching/preloading, и от того, что не загружаете все подряд. Guess.js — это идеальный баланс.
Сейчас Guess.js работает из коробки с Angular, Nuxt js, Next.js и Gatsby. Подключение очень легкое.
Поговорим о Click-ах
Что еще можно сделать, чтобы уменьшить ожидание?<div (сlick)="onClick()">
Это button! Привет Вадим
</div>
Как предугадать, на что кликнет пользователь? Есть очевидный ответ.
У нас есть событие, которое называется mousedown. Оно срабатывает в среднем на 100-200 мс раньше, чем событие Click.
Применяется очень просто:
<div (onmousedown)="onClick()">
Это button! Привет Вадим
</div>
Просто поменяв click на mousedown, мы можем выиграть 100-200 мс.
Но мы можем заранее предсказать, что пользователь кликнет на ссылку, еще до того как сработает не только mousedown но и hoverОчень важное замечание (спасибо RouR что обратил внимание на это), На mousedown вы можете только начать скачивать ресурсы следующей страницы, предзагружать контент, в общем выполнять любую подготовительную работу и только после события click — «визуально» выполнять действие (выполнять переход, обрисовывать элементы)
Если вы будете выполнять действие на mousedown или hover - вы сломаете не только UX но и Accessibility. Будьте осторожны
Я пытаюсь кликнуть на ссылку, и сайт мне говорит, что он знал об этом на 240-500 мс раньше того, как я это сделал .
Опять магия ML? Нет. Существует паттерн: когда мы хотим на что-то кликнуть, мы замедляем движение (чтобы было легче попасть мышкой на нужный элемент).
Есть библиотека, которая анализирует с какой скоростью замедляется движение мыши, и благодаря этому может предсказать, куда я кликну.
Библиотека называется futurelink. Ее можно использовать абсолютно с любым фреймворком:
var futurelink = require('futurelink');
futurelink({
links: document.querySelectorAll('a'),
future: function (link) {
// `link` is probably going to be clicked soon
// Preload everything you can!
}
});
Вы передаете ей те DOM элементы, которые должны участвовать в прогнозе. Как только библиотека предсказывает нажатие на элемент, сработает callback. Вы можете его обработать и начать загружать страницу или залогинить пользователя заранее.
Что пользователь хочет получить при переходе на страницу? В большинстве сценариев: HTML, CSS и немного картинок.
Все это можно реализовать за счет серверного рендеринга SSR.
В Angular для этого достаточно добавить одну команду:
ng add @nguniversal/express-engine
В большинстве случаев это работает замечательно, и у вас появится Server-Side Rendering.
Но что, если вы не на Angular? Или у вас большой проект, и вы понимаете, что внедрение серверного рендеринга потребует довольно большого количества усилий?
Здесь можно воспользоваться статическим prerender: отрендерить страницы заранее, превратить их в HTML. Для этого есть классный плагин для webpack, который называется PrerenderSPAPlugin:
const path = require('path')
const PrerenderSPAPlugin = require('prerender-spa-plugin')
module.exports = {
plugins: [
...
new PrerenderSPAPlugin({
staticDir: path.join(__dirname, 'dist'),
routes: [ '/', '/about', '/some/deep/nested/route' ],
})
]
}
В нем нужно указываете директорию, куда вы хотите сохранять пререндеренные urls, и перечислить те, для которых нужно отрендерить контент заранее.
Но вы можете сделать все еще проще: зайти в свое SPA приложение и написать:
document.documentElement.outerHTML
получить пререндеренный HTML и воспользоваться им: сохранить это в файл. И вот у вас за пару минут появилась пререндеренная страница. Если ваша страница меняется очень редко, это неплохой вариант (как бы глупо он ни выглядел).
Заключение
Несмотря на то что Perceived Performance — очень субъективная метрика, ее можно и нужно измерять. Об этом говорилось в докладе Виктора Русаковича на FrontendConf 2019.Он рассказывал о том, что в Скелетоне есть анимация в плавном фоне, и слева направо она воспринимается на 68% быстрее, чем справа налево. Есть разные исследования, которые показывают, что неправильно примененные техники Perceived Performance могут визуально сделать сайт хуже. Поэтому обязательно тестируйте.
Сделать это можно при помощи сервиса под названием Яндекс.Толока.
Он позволяет выбирать, какой из двух объектов лучше. Можно сделать два видео с разной анимацией и предложить пользователям оценить, какой из них быстрее. Это стоит недорого, скейлится очень хорошо. В сервисе зарегистрировано огромное количество людей из разных регионов, то есть можно делать разнообразные выборки по разным параметрам. Благодаря этому есть возможность провести серьезное UX-исследование за небольшую сумму.
Даже если в конце концов вашему начальнику покажется, что быстрее не стало, проведите исследование и попробуйте улучшить ситуацию с помощью Perceived Performance.
Perceived performance — это низковисящий фрукт почти в любой компании. Именно сейчас у вас есть возможность, применив вышеперечисленные техники, улучшить воспринимаемую производительность и сделать пользователей и менеджеров счастливыми.
Подписывайтесь на Telegram канал obenjiro_notes и Twitter obenjiro.
Хотите стать спикером FrontendConf 2021? Если вам есть что сказать, вы хотите подискутировать или поделиться опытом — подавайте заявку на доклад
Билеты можно приобрести здесь. Вы можете успеть купить их до повышения цены!
Следите за нашими новостями — Telegram, Twitter, VK и FB и присоединяйтесь к обсуждениям.