Русский
Русский
English
Статистика
Реклама

Клиентская оптимизация

Перевод Сравнение производительности CSS и CSS-in-JS в реальном мире

15.05.2021 18:08:01 | Автор: admin
Технология CSS-in-JS заняла прочное место среди инструментов фронтенд-разработки. И возникает ощущение, что CSS-in-JS-тренд в ближайшем будущем лишь усилится. Особенно в мире React. Например, в исследовании State of CSS, проведённом в 2020 году, приняли участие 11492 человека. Лишь 14,3% из них не слышали о Styled Components (о ведущей CSS-in-JS-библиотеке). А вот пользовались этой библиотекой более 40% участников исследования.



Мне уже давно хотелось найти серьёзный материал, посвящённый сравнению производительности CSS-in-JS-библиотек, вроде Styled Components, и доброго старого CSS. Но я, к сожалению, ничего такого, вроде сравнения их производительности на реальном проекте, а не на каком-то простом наборе тестов, найти не смог. Поэтому я решил сам сделать такое сравнение. Я перевёл реальное приложение со Styled Components на Linaria, на библиотеку, которая выполняет извлечение CSS в файлы во время сборки проекта. В результате в приложении, использующем Linaria, не выполняется генерирование стилей во время работы этого приложения на компьютере пользователя.

Прежде чем мы приступим к делу хочу прояснить некоторые вещи. Я не отношу себя к людям, которые ненавидят CSS-in-JS. Я признаю то, что эта технология отличается отличным опытом разработчика (Developer Experience, DX), и то, что она обладает замечательной моделью композиции, унаследованной от React. CSS-in-JS способна дать разработчикам много хорошего (почитать об этом можно здесь). Да и я сам пользуюсь библиотекой Styled Components в нескольких собственных проектах и в проектах, над которыми мне доводилось работать. Но мне всегда было интересно знать о том, сколько пользователям веб-проектов приходится платить за те удобства, которые даёт разработчикам CSS-in-JS.

Да, если вас интересуют лишь мои выводы то вот они: не используйте CSS-in-JS с вычислением стилей во время работы программы в том случае, если вы заботитесь о скорости загрузки вашего сайта. Тут всё просто: чем меньше JavaScript-кода тем быстрее сайт. И с этим ничего особо поделать нельзя. Если же вам интересно узнать о том, как я пришёл к таким выводам продолжайте читать.

Что и как я измерял


Приложение, которое я использовал в тестах это вполне обычный React-проект. Его основа создана с помощью Create React App (CRA), в нём используется Redux и Styled Components (v5). Это достаточно большое приложение с множеством экранов, с настраиваемой панелью управления, с поддержкой тем и со многими другими возможностями. Так как оно было создано с помощью CRA оно не поддерживает серверный рендеринг, в результате всё рендерится на стороне клиента (речь идёт о B2B-приложении, в перечне требований к нему серверного рендеринга не было).

Я взял это приложение и заменил Styled Components на библиотеку Linaria, которая, как мне казалось, имеет похожий API. Я полагал, что перейти со Styled Components на Linaria будет просто. Но, на самом деле, перевод приложения на новую библиотеку стилизации потребовал определённых усилий. А именно, на то, чтобы перевести приложение на Linaria, у меня ушло два месяца. Но даже после того, как у меня получилось что-то такое, с чем уже можно было работать, переведены были лишь несколько страниц, а не всё приложение. Подозреваю, что именно поэтому никто и не проводит таких сравнений, которое решил провести я. Единственное изменение, которое я внёс в приложение, было представлено заменой одной библиотеки стилизации на другую. Всё остальное осталось нетронутым.

Для запуска различных тестов, направленных на исследование двух страниц, которые используются чаще всего, я пользовался инструментами разработчика Chrome. Я всегда запускал тесты по три раза. Представленные здесь цифры это средние показатели по трём запускам тестов. Во всех тестах я устанавливал, на вкладке Performance, значение 4x slowdown для параметра CPU и значение Slow 3G для параметра Network. Для исследования производительности я использовал отдельный профиль Chrome без каких-либо расширений.

Вот какие испытания я провёл:

  1. Анализ сетевой активности приложения (размер JS- и CSS-ресурсов, анализ используемого кода, количество запросов).
  2. Исследование производительности в Lighthouse (аудит производительности с применением мобильных предустановок).
  3. Профилирование производительности (исследование загрузки страниц и особенностей drag-and-drop-взаимодействия с ними).

Анализ сетевой активности приложения


Начнём с анализа сетевой активности приложения. Одной из сильных сторон CSS-in-JS является тот факт, что при использовании этой технологии в приложение не попадает ненужных стилей. Верно? Ну, на самом деле, это не совсем так. Когда на странице имеется лишь один активный стиль, вместе с ним могут загрузиться и ненужные стили. Но эти стили находятся не в отдельном файле, а в JS-бандле.

Вот данные, полученные при исследовании домашней страницы двух вариантов приложения. Один из них, напомню, создан с использованием Styled Components, а второй с помощью Linaria. Показатель до косой черты это размер данных, сжатых gzip, а после косой черты идёт размер несжатых данных.

Сравнение сетевых показателей домашней страницы двух вариантов приложения.

Styled Components Linaria
Общее количество запросов 11 13
Общий размер 361Кб/1,8MB 356Кб/1,8Мб
Размер CSS 2,3Кб/7,2Кб 14,7Кб/71,5Кб
Количество CSS-запросов 1 3
Размер JS 322Кб/1,8Мб 305Кб/1,7Мб
Количество JS-запросов 6 6

Сравнение сетевых показателей поисковой страницы двух вариантов приложения.

Styled Components Linaria
Общее количество запросов 10 12
Общий размер 395Кб/1,9Мб 391Кб/1,9Мб
Размер CSS 2,3Кб/7,2Кб 16,0Кб/70,0Кб
Количество CSS-запросов 1 3
Размер JS 363Кб/1,9Мб 345Кб /1,8Мб
Количество JS-запросов 6 6

Даже несмотря на то, что в Linaria-варианте приложения значительно возрос объём загружаемого CSS-кода, общий объём загружаемых данных снизился у обеих страниц (хотя в данном случае эта разница почти незаметна). Но самое важное тут то, что общий объём CSS- и JS-данных Linaria-варианта страниц меньше, чем размер JS-бандла того варианта приложения, в котором используется Styled Components.

Анализ используемого кода


Если проанализировать объём используемого кода, то окажется, что в Linaria-варианте приложения имеется большой объём (около 55 Кб) неиспользуемого CSS-кода. А в приложении, где применяется Styled Components это всего 6 Кб (причём это CSS из npm-пакета, а не из самой библиотеки Styled Components). Размер неиспользуемого JS-кода в Linaria-варианте приложения на 20 Кб меньше, чем в Styled Components-варианте. Но общий объём неиспользуемого кода больше там, где применяется Linaria. Это один из компромиссов, на которые приходится идти тому, кто использует внешний CSS.

Анализ используемого кода домашней страницы.

Styled Components Linaria
Размер неиспользуемого CSS 6,5Кб 55,6Кб
Размер неиспользуемого JS 932Кб 915Кб
Общий размер 938,5Кб 970,6Кб

Анализ используемого кода поисковой страницы.

Styled Components Linaria
Размер неиспользуемого CSS 6,3Кб 52,9Кб
Размер неиспользуемого JS 937Кб 912Кб
Общий размер 938,5Кб 970,6Кб

Аудит производительности в Lighthouse


Если уж мы говорим об анализе производительности непростительно будет не взглянуть на то, что выдаёт Lighthouse. Сравнение показателей (средние значения после трёх запусков Lighthouse) можно видеть на нижеприведённых диаграммах. Тут, помимо показателей группы Web Vitals, имеются ещё два показателя Main thread work и Execution time. Main thread work это время парсинга, компиляции и запуска ресурсов, большая часть которого уходит на работу с JS, хотя вклад в этот показатель вносят и подготовка макета страницы, и вычисление стилей, и вывод данных, и другие процессы. Execution time это время выполнения JS-кода. Я не включил сюда показатель Cumulative Layout Shift, так как он близок к нулю, и он выглядит практически одинаково для вариантов приложения, в котором используется Linaria и Styled Components.


Показатели Lighthouse для домашней страницы


Показатели Lighthouse для поисковой страницы

Как видите, Linaria-вариант приложения лучше, чем Styled Components-вариант, выглядит в Web Vitals-тестах (он показал худший результат лишь однажды, по показателю CLS). Иногда преимущество оказывается довольно-таки значительным. Например, на домашней странице показатель LCP оказывается лучше на 870 мс, а на поисковой странице на 1,2 с. Страница, на которой используется обычный CSS, не только быстрее рендерится, но и требует меньше ресурсов. А время блокировки и время, необходимое на выполнение всего JS-кода, соответственно, меньше на 300 мс и примерно на 1,3 с.

Профилирование производительности


Lighthouse может дать нам много интересных сведений о производительности приложений. Но нет лучшего средства для детального анализа производительности, чем вкладка инструментов разработчика Chrome Performance. В данном случае то, что удалось выяснить с помощью инструментов этой вкладки, согласуется с тем, что мы уже видели после выполнения Lighthouse-испытаний. Взгляните на следующие диаграммы.


Профилирование производительности домашней страницы


Профилирование производительности поисковой страницы

На страницах, при создании которых используется библиотека Styled Components, имеется больше задач, выполняющихся длительное время. И на завершение этих задач, в сравнении с Linaria-вариантом страниц, нужно больше времени.

Для того чтобы рассмотреть данные профилирования производительности под несколько иным углом, ниже я привёл совмещённые графики загрузки Styled Components-варианта домашней страницы (выше) и её Linaria-варианта (ниже).


Сравнение процесса загрузки разных вариантов домашней страницы

Сравнение особенностей drag-and-drop-взаимодействия со страницами


Я решил сравнить страницы не только по показателям их загрузки, но и на предмет их быстродействия при работе с ними. А именно, я измерил производительность страниц при выполнении действий, предусматривающих перетаскивание элементов и размещение их по группам. Итоговые результаты приведены ниже. Как видно, даже в этом тесте Linaria побеждает Styled Components в нескольких категориях.

Styled Components Linaria Разница
Показатель Scripting, мс 2955 2392 -563
Показатель Rendering, мс 3002 2525 -477
Показатель Painting, мс 329 313 -16
Общее время блокировки, мс 1862,66 994,07 -868


Сравнение процесса взаимодействия с разными вариантами страницы

Итоги


Вот и всё. Как видите, использование технологии CSS-in-JS, предусматривающей вычисление стилей во время работы страницы, оказывает заметное воздействие на производительность. Это актуально, в основном, для недорогих устройств и для регионов с медленными или дорогими интернет-каналами. Поэтому, возможно, нам следует ответственнее подходить к тому, какие именно инструменты мы выбираем, и к тому, как именно ими пользуемся. Отличный опыт разработчика не должен достигаться ценой ухудшения пользовательского опыта.

Я полагаю, что мы (разработчики) должны больше размышлять о том, каковы последствия выбора тех или иных инструментов. Когда я в следующий раз начну работу над новым проектом технологией CSS-in-JS я больше пользоваться не буду. Я либо применю обычный CSS, либо воспользуюсь альтернативой CSS-in-JS, библиотекой, занимающейся обработкой стилей во время сборки проекта и извлекающей стили из JS-бандлов.

Я думаю, что следующим значительным феноменом мира CSS станут CSS-in-JS-библиотеки, обрабатывающие стили во время сборки проектов. Дело в том, что появляется всё больше и больше таких библиотек (например свежайшая vanilla-extract от Seek). Да и крупные компании тоже двигаются в этом направлении, например Facebook.

Как вы относитесь к CSS-in-JS?


Подробнее..

USB over IP удалённое администрирование

21.06.2021 16:11:25 | Автор: admin

Большую часть своей жизни я жил и работал в Москве. Занимался тем, что менял здоровье на деньги. Правда, на очень хорошие, поэтому под занавес пятого десятка купил квартиру в тихом приморском городке, далеко от столичной суеты и ежедневной нервотрёпки.

Особой необходимости в работе у меня не было. Дети уже стали самостоятельными настолько, что запросто могут содержать нас с женой. Да и скопить удалось прилично. Однако, были три проблемы.

Работа в тихом городке

Во-первых до пенсии неплохо хоть где-то формально числиться. Во-вторых, без работы скучно. В-третьих, даже небольшие деньги лишними никогда не бывают.

Однако, в маленьком приморском городке работы для айтишника не было никакой, а ездить в областной центр мне совсем не хотелось. Так я стал ИП и занялся дистанционным обслуживанием компаний, которым было накладно содержать постоянного админа.

Буквально за пару недель я обзавёлся первыми клиентами небольшими компаниями-мелкооптовиками. В штате 10-50 работников, главбух на аутсорсе, сервер с 1С и одним общим для всех каталогом. Почту используют бесплатную, вопросами ИБ заморачиваются по минимуму. В-общем, достаточно типичный для нашей страны мелкий бизнес.

Иногда приходилось выезжать на место, но не часто. Большинство задач вполне решалось дистанционно либо через TeamViewer, либо по телефону. Меня это устраивало, клиентов тоже.

Токены головная боль

Например, надо срочно отправить счёт. Сотрудник вводит пароль, входит в Диадок, загружает документ Но подписать его не может его коллега вчера сунул ключ себе в карман, а сегодня не пришёл на работу, поскольку с утра поехал утрясать какие-то вопросы с партнёром. Приходится отгружать товар без оплаты. Это означает, что деньги придут с большой задержкой как известно, уже оказанная услуга услугой не считается.

Разумеется, меня в этом ни один заказчик не упрекал. Но почти все постоянно намекали, что было бы неплохо как-то раз и навсегда решить проблему с токенами техническими средствами, поскольку административными уже пытались, но безрезультатно.

Всё действительно сложно

Сначала задача показалась мне простой. Технология USB over IP существует достаточно давно все токены подключены к одному устройству, а сотрудники работают с ними, как с локальными. Ничего не теряется, поскольку не покидает пределов офиса.

Осталось полчасика погуглить, найти решение подешевле и предложить заказчикам. Но ни тут-то было. Поразмыслив, я пришёл к выводу, что такой проект значительно сложнее, чем казался.

Прежде всего я выяснил, сколько токенов используют мои заказчики. Оказалось, от двух до одиннадцати. Стало быть, нужно такое решение одного производителя, чтобы удовлетворить всех, причём с небольшим запасом.

Почему одного производителя? Потому что мудрый Оккам умножения сущностей не приветствовал от этого одни проблемы. Да и дешевле так выходит, как правило.

Поскольку айтишников на фултайме ни в одной конторе не было, самодельные и софтовые решения исключались. Делать, настраивать и чинить некому.

По масштабированию прекрасно подходят облака. Но цены у них откровенно кусаются. Например, сервис FlexiHub обойдётся заказчику более 10 тыс. рублей в месяц за 5 ключей, не считая стоимости хаба. И это без гарантий совместимости.

Но самое главное сервисы требуют регулярной своевременной оплаты. У небольшой компании с этим могут быть проблемы. У неё не всегда есть возможность даже зарплаты сотрудникам заплатить вовремя. Задержка в неделю-другую дело обыкновенное.

В-общем, нужна железка. Точнее, набор устройств с разным количеством портов. Как я писал выше: от двух до одиннадцати с небольшим запасом. Чтобы один раз купил и навсегда твоё.

Если ориентироваться на мировые бренды, то надо брать Digi AnywhereUSB. За 2 USB-порта больше 20 тыс. рублей, за 14 портов в районе 150 тыс. рублей. Для небольших компаний это явно дорого.

Конечная остановка USB over IP концентратор

Перебирая варианты я пришёл к отечественному аппаратно-программному решению DistKontrolUSB. На нём и остановился. Причин этому несколько.

Прежде всего цена. За концентратор на 4 порта мой мелкий клиент заплатит 26 тыс. рублей с хвостиком, а более крупным придётся выложить около 69 тыс. рублей, но за 16 портов. Дешевле западного аналога в два раза.

Второе достоинство отечественного разработчика знание российской специфики. Младшая версия концентратора на 4 порта имеет ограниченную функциональность. В ней отсутствует авторизации для подключения к токенам, нет ограничений доступа и управления питанием портов.

Такой подход разумен, поскольку в небольших организациях все ключи по сути являются общими достаточно одной авторизации для всего. На практике это означает, что даже если реализовать отдельный вход для каждого токена, то получится всего лишь замедление работы сотрудников на время дополнительной аутентификации. Да ещё и возникнет путаница в паролях, что рано или поздно всё равно закончится уменьшением количества пар логин-пароль до одной.

Что касается управления питанием портов, то при их небольшом количестве функция лишена смысла. Проще перезагрузить концентратор полностью. Или руками перевоткнуть зависший USB-ключ.

Таким образом, большинство моих клиентов, обходящихся 2-4 ключами, за относительно небольшую сумму получат надёжное устройство для дистанционной работы с токенами. Причём, без заведомо избыточной функциональности, за которую надо платить.

Третье достоинство удобное удалённое управление концентратором через веб-интерфейс. Это особенно важно для более крупных организаций, которым подойдёт концентратор на 16 портов. Сотрудников там больше и без каких-то элементов ИБ не обойтись.

Допустим, по соображением безопасности требуется ограничить доступ к порту по IP-адресу, а к ключу по логину и паролю. Через панель управления это делается мгновенно, причём в одном модуле. Надо всего два раза сдвинуть соответствующие ползунки и нажать на кнопу Сохранить. Изменения сразу вступят в силу.

Проблема нахождения концов актуальна не только для очень крупных организаций, но и для моих клиентов. При помощи системного журнала в WEB-интерфейсе Управляемого USB over IP концентратора решение этой задачи облегчается. В журнале хранится вся информация о подключениях и отключениях, включая логин пользователя и IP-адрес, с которого было осуществлено подключение. Кстати, пару раз пригодилось.

Раз уж дело дошло до практики, то вот ещё один пример. Хоть правило уходя гасите свет существует, но выполняется не всегда. Один мой заказчик опасался, что в ночное время какой-нибудь хакер взломает концентратор и наделает делов.

В настройках USB over IP концентратора было сформировано автоматическое задание, по которому устройство отключается в заданное время. А чтобы директор не волновался и спал спокойно он получает уведомление о выполнении на электронную почту.

Итоги

Проект по оптимизации работы с токенами я начал в январе этого года. Из семи предприятий, с которыми я работаю, три уже приобрели и успешно используют USB over IP концентраторы. В одной организации это запланировано на лето. Думаю, что и оставшиеся три до конца года примут положительное решение.

Тем более, что моя аргументация теперь основана не только на умозрительных аргументах, но и на практике эксплуатации. Поскольку теперь я что-то смыслю в торговле, то могу оперировать цифрами.

Например, дневная задержка в оплате счёта на один миллион стоит от 100 до 1000 рублей в зависимости от договора. С отчётностью ещё хуже 5% от суммы налога. А это уже несколько тысяч даже для небольшой фирмочки. Причём, только прямого ущерба о каких-то сорвавшихся сделках я даже не говорю.

Сумму убытков из-за подобных задержек владелец компании знает. Стоимость USB over IP концентратора тоже. Срок окупаемости вычисляется без всякой высшей математики.

Насколько я могу судить, первопроходцы уже ушли в плюс. Даже те, кто приобрёл устройство на 16 портов.

Внимательный читатель заметит, что я ничего не написал про стоимость обслуживания. Исправляюсь оно ничего не стоит. По крайней мере для моих клиентов, поскольку работы у меня не прибавилось.

Впрочем, вру. Мои клиенты уже начали рекомендовать меня своим знакомым. Я думаю, что по уровню заработка я тут потихоньку достигну своего бывшего московского уровня. Жалею, что не уехал из Москвы раньше.

Подробнее..

Проблемы рендера 7-и тысяч элементов на Vuetify

03.06.2021 08:11:45 | Автор: admin

Предисловие

На момент написания статьи я готовился к диплому и писал дипломный проект для нужд Московского Политеха. Моя задача - перенести существующий функционал из PHP-таблицы во что-то современное с кучей проверок, после чего дополнить данный функционал. Движок - Nuxt, материал-фреймворк: Vuetify.

После написания первичного кода, я, довольный, окинул взглядом свою таблицу и пошел спать. На следующий день мне предстояло импортировать 150+ проектов заказчика в свою таблицу. После импорта, я удивился тому, что браузер повис. Ну, бывает, просто переоткрою вкладку. Не помогло. Я впервые столкнулся с проблемой, что я рендерю слишком много, как для движка, так и для самого браузера. Пришлось начать думать.

Первые попытки

Что делает разработчик, когда сталкивается с проблемой? Идет гуглить. Это было первое, что я сделал. Как оказалось, проблема медленного рендера таблицы Vuetify встречается с куда меньшим числом элементов, чем у меня. Что советуют:

  • Рендерить элементы по частям через setInterval

  • Ставить условие, чтобы не рендерить элементы, пока не сработает хук жизненного цикла mounted()

  • Использовать v-lazy для последовательной отрисовки

При этом было предложение использовать компонент Virtual Scroller, позволяющий отрисовывать элементы по мере скролла, а предыдущие разрендеривать. Но этот компонент Vuetify не работает с таблицами Vuetify -_-

С "радостью" прочитав, что в Vuetify 3 (релиз через ~полгода) производительность улучшится на 50%, я стал пробовать решения. Рендер элементов по частям ничего не дал, так как на условном тысячном элементе отрисовка начинала лагать, а к семи тысячам снова всё висло. Рендер элементов на mounted не дал вообще ничего, всё зависало, но зато после того, как страница загрузится (эээ, ура?). v-lazy хоть и рендерился быстрее, но рендерить 14 тысяч компонентов (Vuetify и Transition от Vue) тоже грустное занятие.

Даже после того, как элементы отрендериваются, скроллить по таблице невыносимо. Элементов много, логики много, браузер просто не выдерживает такого. Сдача всего этого стояла на следующий день. Нужно было выкручиваться, учитывая опыт StackOverflow, который хоть мне и не помог, но натолкнул на то, что делать на следующем шаге.

ТаблицаТаблица

Решение 1. Intersection Observer

Итак, что мы имеем. v-lazy отрисовывать невозможно, это 14 тысяч компонентов. Vuetify Virtual Scroller не поддерживается в Vuetify Data Table из-за его структуры. Выходит, нужно писать свою реализацию. Кто умеет определять, докрутил ли пользователь до элемента? Intersection Observer. Internet Explorer нам не нужен, так что можем приступать.

Первая логичная попытка: использовать директиву v-intersect от самого Vuetify. И 7 тысяч директив также привели к длительному рендеру страницы =(. Значит, выбора нет и придется работать руками.

mounted() {  //Цепляем его на таблицу с overflow: auto  //Требуемая видимость элемента для триггера: 10%this.observer = new IntersectionObserver(this.handleObserve, { root: this.$refs.table as any, threshold: 0.1 });//Почему нельзя повесить observe на все нужные элементы? Ну ладноfor (const element of Array.from(document.querySelectorAll('#intersectionElement'))) {this.observer.observe(element);}},

Теперь взглянем на сам handleObserve:

async handleObserve(entries: IntersectionObserverEntry[]) {const parsedEntries = entries.map(entry => {const target = entry.target as HTMLElement;  //Предварительно задали data-атрибутыconst project = +(target.dataset.projectId || '0');const speciality = +(target.dataset.specialityId || '0');return {          isIntersecting: entry.isIntersecting,          project,          speciality,};    });//Чтобы точно было реактивно    this.$set(this, 'observing', [      //Не добавляем дубликаты      ...parsedEntries.filter(x => x.isIntersecting && !this.observing.some(y => y.project === x.project && y.speciality === x.speciality)),      //Убираем пропавшие      ...this.observing.filter(entry => !parsedEntries.some(x => !x.isIntersecting && x.project === entry.project && x.speciality === entry.speciality)),     ]);//Иначе функция стриггерится несколько раз     Array.from(document.querySelectorAll('#intersectionElement')).forEach((target) => this.observer?.unobserve(target));     //Даем Vuetify перерендериться await this.$nextTick(); //Ждем 300мс, чтобы не триггернуть лишний раз, отрисовка тоже грузит браузер     await new Promise((resolve) => setTimeout(resolve, 500));     //Вновь обсервим элементы     Array.from(document.querySelectorAll('#intersectionElement'))          .forEach((target) => this.observer?.observe(target));},

Итак, мы имеем 7 тысяч элементов, на которых смотрит наш Intersection Observer. Есть переменная observing, в которой содержатся все элементы с projectId и specialityId, по которым мы можем определять, нужно ли показывать нужный элемент в таблице. Осталось всего-лишь повесить v-if на нужный нам элемент и отрисовывать вместо него какую-нибудь заглушку. Ура!

 <template #[`item.speciality-${speciality.id}`]="{item, headers}" v-for="speciality in getSpecialities()"><div id="intersectionElement" :data-project-id="item.id" :data-speciality-id="speciality.id"><ranking-projects-table-itemv-if="observing.some(x => x.project === item.id && x.speciality === speciality.id)":speciality="speciality":project="item"/><template v-else>Загрузка...</template></div></template>

А на саму таблицу вешаем v-once. Таблице будет запрещено менять свой рендер без $forceUpdate. Не очень красивое решение, но Vuetify непонятно чем занимается при скролле, запретим ему это делать.

<v-data-table v-bind="getTableSettings()"   v-once   :items="projects"   @update:expanded="$forceUpdate()">

Итоги:

  • Рендер занимает около секунды

  • Элементы разрендериваются вне их зоны видимости

  • Идет ререндер на каждое действие внутри таблицы

Но при этом работать с большой таблицей невыносимо. 7 тысяч событий на каждой клетке таблицы, бесконечный набор сообщений "Загрузка...". Таблица была создана, чтобы выполнять действия быстро, и с этим решением мы релизнулись.

Загрузка...Загрузка...

Однако было ясно (и заказчик это подчеркнул), что работать с таблицей тяжело. Нужно было что-то решать, но на момент тогда я зашел в тупик: Vuetify Data Table имеет ужасные проблемы с производительностью, которые признают даже разработчики фреймворка.

Не переписывать же мне их таблицу и создавать свой аналог?

Решение 2. Переписать таблицу и создать свой аналог

Решение, приведённое выше, может подойти для таблиц с куда меньшим количеством компонентов, содержимого и прочего. Но у нас с нашей логикой это позволило работать с таблицей, пренебрегая комфортом пользователей. Попробуем добавить немного комфорта.

Что мы поняли из первого решения:

  1. Таблицы Vuetify лагучий отстой

  2. Если показывать элементы только когда пользователь их увидит, рендер будет быстрее

  3. Нужно как-то сократить время на первичную отрисовку, чтобы пользователь не видел бесконечную "Загрузку"

  4. Можно заставить Intersection Observer реагировать чаще, чтобы рендерить элементы по мере скролла, а не когда скролл остановится или среагирует событие (300мс задержка)

Возвращаемся к Virtual Scroller. Он не работает в таблице Vuetify, так? А что если мы напишем свою таблицу с блекджеком и display: grid? Зачем-то же его придумали.

Что нужно для Virtual Scroller? Фиксированная высота каждого элемента. Что нужно для Grid'ов? Фиксированная ширина каждого элемента и информация о количестве элементов. Бахнем CSS-переменные для последующего использования в CSS:

<div class="ranking-table" :style="{    '--projects-count': getSettings().projectsCount,    '--specialities-count': getSettings().specialitiesCount,    '--first-column-width': `${getSettings().firstColumnWidth}px`,    '--others-columns-width': `${getSettings().othersColumnsWidth}px`,    '--cell-width': `${getSettings().firstColumnWidth + getSettings().othersColumnsWidth * getSettings().specialitiesCount}px`,    '--item-height': `${getSettings().itemHeight}px`  }">

Потом пишем гриды по типу аля

display: grid;grid-template-columns:var(--first-column-width)repeat(var(--specialities-count), var(--others-columns-width));

И так далее для элементов внутри. Класс! А еще мы, зная ширину и количество элементов, можем задать ширину нашему Virtual Scroller (он сам по умолчанию туповат), а заодно и всем нашим элементам, чтобы не дай боже кто-то вышел за доступные ему границы

.ranking-table_v2__scroll::v-deep {.v-virtual-scroll {&__container, &__item, .ranking-table_v2__project {    width: var(--cell-width);}}}

Примечание: если вы сделаете просто <style>без scoped и решите, что будет хорошей идеей редактировать глобальные стили вне окружения компонента, то у меня для вас плохие новости: лучше так не делать вне каких-то App.vue, и стоит ознакомиться с тем, что это за v-deep.

Поехали: добавляем Virtual Scroller, пихаем в него проекты, после чего выводим последние. Сразу скажу: поддержки Expandable Items у нас тут нет, я вынес информацию о проекте во всплывающее окно. Жаль, конечно, что нельзя это отображать прямо в таблице, как делал Vuetify, но тогда придется помучаться с их скроллером, а он и так не особо хорошо работает. В общем, к делу:

<v-virtual-scroll   class="ranking-table_v2__scroll"   :height="getSettings().commonHeight":item-height="getSettings().itemHeight":items="projects"><template #default="{item}"><div class="ranking-table_v2__project" :key="item.id"><!-- ... -->

Итого: на страницу, допустим, помещается 6 проектов (высота же у всех максимальная по факту), итого рендерится 6 строк + шапка. Колонок 50. Итого рендерится около 300 сложных компонентов. А вот это уже задача не уровня мстителей, 300 мы рендерить умеем.

Вспоминаем про лучший инструмент всех времен и народов v-lazy: он позволяет отрендерить элемент один раз и потом не перерендеривать его. Раньше мы пытались отрендерить 14 тысяч компонентов, сейчас 600 простых. Ну и оборачиваем все наши колонки (кроме шапки) в v-lazy. При горизонтальном скролле элементы подгружаются, и остаются отрендеренными до тех пор, пока сама строка таблицы не пропадет из области видимости и не разрендерится.

 <v-lazy class="ranking-table_v2__item ranking-table_v2__item--speciality"v-for="(speciality, index) in specialities":key="speciality.id">   <!-- Колонка в строке таблицы --></v-lazy>

Видео с разницей, можно сравнить:

Плюсы такого решения:

  • Элементы перестали скакать как ненормальные

  • Нет нужды писать свою реализацию логики рендера/отрендера

  • Можем отказаться от v-once и всяких принудительных ререндеров аля $forceUpdate

  • Куда больше гибкости при верстке таблицы

Минусы:

  • Если используются выпадающие элементы (expand), нужно писать свою реализацию

  • Нужно верстать таблицу самому, без инструментов движка

  • Нет средств сортировки/группировки и прочего (мне было не нужно)

  • Для того, чтобы шапка всегда была отрендеренной, но скроллилась вместе со всеми, мне пришлось отслеживать событие Scroll и подгонять скролл шапки к скроллу основных элементов

  • Чтобы отображать таблицу в нужном мне проценте от высоты страницы, мне пришлось смотреть на window.innerHeight и применять его в CSS переменных и в значении высоты у VirtualScroll

Несмотря на то, что минусов визуально больше, значительное улучшение UX и скорости отрисовки значительно компенсирует затраты времени и изучения инструментов для реализации этой штуки.

Заключение

Я привел 2 варианта решения проблем с производительностью отрисовки таблиц в Vuetify. Оба решения достаточно сложные, просто сложные в разных аспектах. Оба так или иначе помогли мне решить мою проблему, и у обоих есть свои ограничения. Например, второй вариант мне нравится больше, но отсутствие средств Vuetify (сортировка/группировка и пр.) может оттолкнуть тех, кому это позарез нужно.

Зачем я это пишу? Я перегуглил половину интернета и не нашел простых решений своей проблемы. Наверное, как раз таки потому, что этих решений нет. Эти таблицы не предназначались для сложной логики, даже в условных гугловских таблицах элементов отрисовывается меньше, потому что там буквально один инпут, изредка с каким-то контентом, здесь же - гигантская куча логики.

И да: я пользовался дебагером производительности от Vue и смотрел, кто её потребляет. Зачастую там был буквально один-два компонента, и, заменив их на какой-то другой с похожей логикой, проблема не решалась - дело в их количестве, а не сложности (не считая таблицу Vuetify - там передается множество props'ов из компонента в компонент).

Надеюсь, что приведенные мной варианты натолкнут кого-то на решение его проблемы, а кто-то просто узнает что-то новое =). Будем вместе ждать стабильный Vue 3 со всей его экосистемой, как минимум Nuxt 3. Что-то обещают множество улучшений, может, часть костылей из этой статьи даже пропадет.

Подробнее..

Дружим Angular с Google

27.08.2020 18:04:37 | Автор: admin

Дружим Angular с Google


Google ненавидит SPA


Когда мы говорим про современные интернет магазины, мы представляем себе тяжелые для понимания серверы, рендрящие тысячи статических страничек. Причем именно эти тысячи отрендеренных страниц одна из причин почему Single Page Applications не прижились в электронной коммерции. Даже крупнейшие магазины электронной коммерции по-прежнему выглядят как куча статических страниц, для пользователя это нескончаемый цикл кликов, ожидания и перезагрузки страниц.


Одностраничные приложения приятно отличаются динамичностью взаимодействия с пользователем и более сложным UX. Но как не прискорбно обычно пользовательский опыт приносится в жертву SEO оптимизации. Для сеошника сайт на angular это такая себе проблема поискаовикам трудно индексировать страницы с динамическим контентом.


Другой недостаток SPA это превью сайта. Например, пользователь только что купил новый телевизор в нашем интернет магазине, и хочет порекомендовать его своим друзьям в соцсетях. Превью ссылки в случае Angular будет выглядеть так:


site preview


Мы любим JS и Angular. Мы верим, что классный и удобный UX может быть построен с на этом стеке технологий, и мы можем решить все сопутствующие проблемы. В какой-то момент мы наткнулись на Angular Universal. Это модуль Angular для рендеринга на стороне сервера. Сначала нам показалось что вот оно решение, но радость была преждевременной и отсутствие больших проектов, с его применением, было доказательством этого. Шесть месяцев назад мы надеялись найти production ready решение, но поняли, что нет больших проектов, написанных на Universal.


В итоге, мы начали разрабатывать компоненты для интернет-магазина, используя обычный Angular 2, и ждали, когда Universal будет объединен с Angular Core. На данный момент слияния проектов еще не поизошло, и пока не ясно, когда это произойдет (или как итоговый вариант будет совместим с текущей реализацией), однако сам Universal уже перекочевал в github репозиторий Angular.


Не смотря на эти трудности, наша цель осталась неизменной создавать классные веб-приложения на Angular с серверным рендерингом для индексации поисковиками. В результате наша команда разработала более 20 универсальных компонентов для быстрой сборки интернет-магазина. Как в итоге это было достигнуто?


Что такое Angular Universal


Прежде всего, давайте обсудим, что такое Angular Universal. Когда мы запустим наше приложение на Angular 2 и откроем исходный код, увидим что-то вроде этого:


<!DOCTYPE html><html><head>  <meta charset="utf-8">  <title>Angular 2 app</title>  <!-- base url -->  <base href="http://personeltest.ru/aways/habr.com/"><body>  <app></app></body></html>

Фронтенд фреймворки, такие как Angular, динамически добавляют контент, стили и данные в тег .
Для приложения ориентированного в первую очередь на пользователя критически важно иметь аналитику по действиям пользователя и по индексации сайта поисковиками. Ну и конечно мы хотим чтобы наш контент индексироваля и был в топе выдачи google.

Для решения проблем c индексацией Angular Universal дает нам возможность выполнять рендеринг на стороне сервера. Наша страница будет создаваться на бэкэнд-сервере, написанном на Node.Js, .NET или другом языке, и браузер пользователя получит страницу со всеми привычными тегами в ней -заголовками, мета-тегами и контентом.


В нашем случае это отлично, потому что краулеры поисковиков очень хорошо подготовлены к индексированию статических веб-страниц. Мы разрабатываем стандартное приложение на Angular, а Universal заботится о серверном рендеринге.


До этого момента все выглядит не плохо? Возможно, но дьявол скрыт в мелочах, как всегда.


Итак, мы хотим поделиться с вами подводными камнями, с которыми мы столкнулись на нашем пути.


Подводные камни Angular Universal


Не трогайте DOM


Когда мы начали тестировать компоненты нашего магазина с помощью Universal, нам пришлось потратить некоторое время, чтобы понять, почему наш сервер падает при запуске без вывода серверной страницы. Например, у нас есть компонент Session Flow component, который отслеживает активность пользователя во время сессии (перемещения пользователя, клики, рефферер, информация об устройстве пользователя и т.д.). После поиска информации в issues на GitHub мы поняли, что в Universal нет обертки над DOM.


DOM на сервере не существует.


Если вы склонируете этот Angular Universal стартер и откроете browser.module.ts вы увидите, что в массиве providers разработчики Universal предоставляют дваboolean значения:


providers: [    { provide: 'isBrowser', useValue: isBrowser },    { provide: 'isNode', useValue: isNode },    ...  ]

Итак, если вы хотите чтобы ренедеринг на сервере работал, вы должны заинжектить в свои компоненты эти переменные и обернуть в проверку среды выполнения фрагменты кода, где непосредственно происходит взаимодействие с DOM. Например, вот так:


@Injectable()export class SessionFlow{    private reffererUrl : string;    constructor(@Inject('isBrowser') private isBrowser){        if(isBrowser){            this.reffererUrl = document.referrer;        }    }}

Universal автоматически добавляет false, если это сервер, и true, если браузер. Может быть, позже разработчики Universal пересмотрят эту реализацию и нам не придется беспокоиться об этом.


Если вы хотите активно взаимодействовать с элементами DOM, используйте сервисы Angular API, такие какElementRef, Renderer или ViewContainer.


Правильный роутинг


Поскольку сервер отражает наше приложение, у нас была проблема с роутингом.


Ваш роутинг на клиенте, написанный на Angular, должен соответствовать роутингу на сервере.


Поэтому не забудьте добавить тот же роутинг на сервере что и на клиенте. Например, у нас есть такие роуты на клиенте:


    [      { path: '', redirectTo: '/home', pathMatch: 'full' },      { path: 'products', component: ProductsComponent },      { path: 'product/:id', component: ProductComponent}    ]

Тогда нужно создать файл server.routes.ts с массивом роутов сервера. Корневой маршрут можно не добавлять:


export const routes: string[] = [  'products',  'product/:id'];

Наконец, добавьте роуты на сервер:


import { routes } from './server.routes';... other server configurationapp.get('/', ngApp);routes.forEach(route => {  app.get(`/${route}`, ngApp);  app.get(`/${route}/*`, ngApp);});

Пререндеринг стартовой страницы


Одной из наиболее важных особенностей Angular Universal является пререндеринг. Из исследования Kissmetrics 47% потребителей ожидают, что веб-страница загрузится за 2 секунды или даже менее. Для нас было очень важно отобразить страницу как можно быстрее. Таким образом, пререндеринг в Universal как раз про нашу задачу. Давайте подробнее рассмотрим, что это такое и как его использовать?


Когда пользователь открывает URL нашего магазина, Universal немедленно возвращает предварительно подготовленную HTML страничку с контентом, а уже затем затем начинает загружать все приложение в фоновом режиме. Как только приложение полностью загрузится, Universal подменяет изначальную страницу нашим приложением. Вы спросите, что будет, если пользователь начнет взаимодействовать со страницей до загрузки приложения? Не беспокойтесь, библиотека Preboot.js запишет все события, которые выполнит пользователь и после загрузки приложения выполнит их уже в приложении.


Чтобы включить пререндеринг, просто добавьте в конфигурацию сервера preboot: true:


res.render('index', {      req,      res,      preboot: true,      baseUrl: '/',      requestUrl: req.originalUrl,      originUrl: `http://localhost:${ app.get('port') }`    });  });

Добавление мета-тегов


В случае SEO-ориентированного сайта добавление мета-тегов очень важно. Например, у нас есть Product component, который представляет страницу определенного продукта. Он должен асинхронно получать данные о продуктах из базы данных и на основе этих данных добавлять мета-теги и другую специальную информацию для поискового робота.


Команда Angular Universal создала сервис angular2-meta, чтобы легко манипулировать мета-тегами. Вставьте мета-сервис в ваш компонент и несколько строк кода добавлят мета-теги в вашу страницу:


import { Meta, MetaDefinition } from './../../angular2-meta';@Component({  selector: 'main-page',  templateUrl: './main-page.component.html',  styleUrls: ['./main-page.component.scss']})export class MainPageComponent {  constructor(private metaService: Meta){    const name: MetaDefinition = {      name: 'application-name',      content: 'application-content'    };    metaService.addTags(name);  }}

В следующей версии Angular этот сервис будет перемещен в @angular/platform-server


Кэширование данных


Angular Universal запускает ваш XHR запрос дважды: один на сервере, а другой при загрузке приложения магазина.


Но зачем нам нужно запрашивать данные на сервере дважды? PatricJs создал пример, как сделать Http-запрос на сервере один раз и закэшировать полученные данные для клиента. Посмотреть исходный код примера можно здесь. Чтобы использовать его заинжекте Model service и вызовите метод get для выполнения http-вызовов с кешированием:


    public data;    constructor(public model: ModelService) {        this.universalInit();    }    universalInit() {        this.model.get('/data.json').subscribe(data => {        this.data = data;        });    }

Выводы


Рендеринг на стороне сервера с помощью Angular Universal позволяет нам
создавать клиентоориентированные приложения электронной коммерции более не переживая об индексации вашего приложения. Кроме того, функция prendering позволяет сразу показать сайт для вашего клиента, улучшая время рендеринга (что довольно неприятный момент для Angular приложений из-за большого размера самой библиотеки).

Подробнее..

Перфоманс фронтенда как современное искусство графики, код, кулстори

17.09.2020 12:16:31 | Автор: admin

Всем привет. В предыдущих статьях мы говорили о базовых вещах оптимизации: раз и два. Сегодня я предлагаю с разбега окунуться в одну часть из тех задач, которыми занимается команда архитектуры фронтенда в hh.ru.


Я работаю в команде архитектуры. Мы не только перекладываем файлики из одной папки в другую, но и занимаемся кучей других вещей:


  • Перфоманс приложения
  • Инфраструктура: сборка, тесты, пайплайны, раскатка на продакшене, инструменты для разработчика (например бабель-плагины, кастомные eslint правила)
  • Дизайн-система (UIKit)
  • Переезд на новые технологии

Если покопаться, можно найти много интересного.


Поэтому, давайте поговорим о перфомансе. Команда фронтенд архитектуры ответственна как за клиентскую часть, так и серверную (SSR).


Я предлагаю посмотреть на метрики и разобраться, как мы реагируем на различные триггеры. Статья будет разбита на 2 составляющие. Серверную и клиентскую. Графики, код и кулстори прилагаются.



В этой статье мы поговорим о том, как трекаем, какие (интересные) результаты есть. Для теории и что-почем лучше обратиться к первой статье.


Клиент


Главная проблема клиентских метрик устройства. Они разные, их очень много, как и систем, браузеров и качества соединения. На основе всех этих данных нужно строить какую-то информацию, которая будет поддаваться анализу.


Процесс построения правильных графиков был не единоразовым событием. Мы построили первую версию метрик. Осознали, что они не работают, провели работу над ошибками, перестроили. Повторили несколько раз для части метрик. В итоге сейчас мы трекаем:


FMP (first meaningful paint)



FMP трекается для двух частей сайта: меню и конец контента. Каждая линия отдельная страница. На графики выводим TOP самых тяжелых страниц. Практически все наши графики отображают 95 перцентиль. Эти не стали исключением.


Тот же график, но с отображением только одной страницы:


Для нас FMP наиболее важный график и вот почему:


  • Сайт hh.ru с точки зрения соискателя текстовый. Открыть поиск, кликнуть на вакансию, прочитать, решить ок или нет, откликнуться.
  • С точки зрения работодателя сайт частично текстовый. Открыть поиск или разобрать отклики это работа с резюме кандидата.

Вопрос правильного измерения FMP один из наиболее важных для нас. Что мы понимаем под FMP? Это количество и время загрузки критических для рендера ресурсов.


Кажется, что FMP может считаться как-то так:


requestAnimationFrame(function() {   // Перед первым рендером взяли время когда renderTree было сформировано  var renderTreeFormed = performance.now();  requestAnimationFrame(function() {    // Здесь данные отрендерены пользователю    var fmp = performance.now();    // Сохраняем для дальнейшей отправки на сервер    window.globalVars.performance.fmp.push({      renderTreeFormed: renderTreeFormed,      fmp: fmp    })  });});

Здесь есть несколько интересных моментов:


  1. Если вставить этот код после меню и перед закрытием body, то получаемые данные могут и будут отличаться (при условии, что у вас вся страница не умещается в 1 экран). Дело в том, что браузеры будут пытаться оптимизировать рендер.
  2. Это решение не работает ()/


Дело в том, что браузер не будет вызывать raf и будет сильно замедлять вызовы setTimeout\interval когда вкладка не является активной. Поэтому мы получим некорректные данные.


Это означает, что в текущем решении нам нужно как-то обрабатывать этот случай. Здесь на помощь приходит PageVisibility API:


window.globalVars = window.globalVars || {};window.globalVars.performance = window.globalVars.performance || {};// Помечаем, была ли страница активна в момент загрузкиwindow.globalVars.performance.pageWasActive = document.visibilityState === "visible";document.addEventListener("visibilitychange", function(e) {    // Если что-то изменилось  реагируем    if (document.visibilityState !== "visible") {        window.globalVars.performance.pageWasActive = false;    }});

Используем полученные знания в FMP:


requestAnimationFrame(function() {   // Перед первым рендером взяли время когда renderTree было сформировано  var renderTreeFormed = performance.now();  requestAnimationFrame(function() {    // Здесь данные отрендерены пользователю    var fmp = performance.now();    // Сохраняем для дальнейшей отправки на сервер,     // только в случае, если страница была все время активной    if (window.globalVars.performance.pageWasActive) {        window.globalVars.performance.fmp.push({          renderTreeFormed: renderTreeFormed,          fmp: fmp        });        }  });});

Окей, теперь мы не отправляем заведомо некорректные данные. Качество данных стало лучше.
Следующая проблема: один из популярнейших паттернов для наших пользователей зайти на страницу поиска, нащелкать "открыть в новой вкладке" вакансий, а затем переключаться по вкладкам. Мы теряем очень много аналитики.


Нужна ли нам такая сложная структура, учитывая, что она теряет данные? На самом деле нет, нам важно определить, когда renderTree (почти) готова, чтобы понимать, загружены ли ресурсы и дошел ли браузер до конца меню, контента.


Поэтому мы решили задачу изящнее. В нужные места мы стали вставлять вот такие метки (и fmp_menu для меню):


<script>window.performance.mark('fmp_body')</script>

На их основе мы и строим графики:


Такой способ удобен: мы всегда можем добавить новую контрольную точку и оптимизировать отображение вакансии, резюме и поисковой выдачи.


Несколько интересностей:


  1. На FMP у нас настроен триггер. Чтобы реагировать на массовые проблемы, он настроен на 3 минуты бесперебойных проблем. Поэтому "одиночные" выбросы просто игнорирует.
  2. Критический FMP: 10 секунд. В эти моменты мы смотрим на проблемные урлы и на выдаваемые нами данные.
  3. У нас было несколько интересных историй, когда FMP начинал зашкаливать. Часто эта метрика может коррелировать с массовыми проблемами с сетью у пользователей, а также с проблемами на наших бекендах. Метрика получилась очень чувствительной
  4. Если брать статистику, то мобильные телефоны получаются производительней настольных машин! Вот пример, на котором я взял время с большой нагрузкой в рабочий день и построил графики по одному url-у. Слева мобильники, справа десктопы, 95 перцентиль:

Разница незначительная, но она есть. Я лично склонен полагать, что телефоны люди обновляют чаще, чем компьютеры.


Вторая метрика TTI


В целом, для работы с TTI более чем достаточно взять готовый код у гугла


У нас для вычисления TTI свой велосипед. Он нам нужен, потому что мы завязываемся внутри на Page Visibility API, о котором я писал выше. К сожалению, TTI полностью завязан на longtask и у нас нет опции "посчитать его как-нибудь по-другому", поэтому мы вырезаем пласт метрик, когда пользователь уходит со страницы.


Посмотреть код TTI
function timeToInteractive() {    // Ожидаемое время TTI    const LONG_TASK_TIME = 2000;    // Максимально ожидаемое время TTI, если не произошло лонгтасок    const MAX_LONG_TASK_TIME = 30000;    const metrics = {        report: 'TTI_WITH_VISIBILITY_API',        mobile: Supports.mobile(),    };    if ('PerformanceObserver' in window && 'PerformanceLongTaskTiming' in window) {        let timeoutIdCheckTTI;        const longTask = [];        const observer = new window.PerformanceObserver((list) => {            for (const entry of list.getEntries()) {                longTask.push(Math.round(entry.startTime + entry.duration));            }        });        observer.observe({ entryTypes: ['longtask'] });        const checkTTI = () => {            if (longTask.length === 0 && performance.now() > MAX_LONG_TASK_TIME) {                clearTimeout(timeoutIdCheckTTI);            }            const eventTime = longTask[longTask.length - 1];            if (eventTime && performance.now() - eventTime >= LONG_TASK_TIME) {                if (window.globalVars?.performance?.pageWasActive) {                    StatsSender.sendMetrics({ ...metrics, tti: eventTime });                }            } else {                timeoutIdCheckTTI = setTimeout(checkTTI, LONG_TASK_TIME);            }        };        checkTTI();    }}export default timeToInteractive;

Выглядит TTI вот так (95", TOP тяжелых урлов):


Может появиться вопрос: почему TTI такой большой? Дело в:


  1. Рекламе, которая грузится по requestIdleCallback
  2. Аналитике
  3. 3d party скриптах

К сожалению, "обстоятельства сильнее нас". Поэтому для аналитики своего кода приходится выкручиваться и мы используем еще немного метрик:


Время инита приложения без гидрейта (рендера)


95" TOP тяжеленьких:


Зачем? Мы понимаем, как много JS кода мы грузим и сколько времени нужно, чтобы он проинитился.
По этому графику мы понимаем, какие страницы наиболее загружены js кодом, при больших выбросах во время релизов, можно сказать, где сломались чанки приложения.


Но самым показательным для нас на клиенте в плане JS рантайма является


Гидрейт


Замеряем время по окончанию инита без рендера, делаем hydrate, который принимает третьим аргументом колбек, в колбеке замеряем еще раз время и сохраняем разницу:



Если совместить график инита и гидрейта, можем сделать несколько выводов:


  1. Если самая тяжелая страница в гидрейте и ините одинаковая, то её тяжесть обоснована огромным количеством разных компонентов и логики, а не данных (большим списком резюме \ вакансий, чем-то еще)
  2. Если график гидрейта не коррелирует с графиком инита проблема в количестве данных для рендера. Здесь выделяются графики поисковых страниц, они сильнее зависят от тяжести данных для рендера:

Чем помогают эти графики?


  1. Как только видим всплеск в FMP, идем в остальные графики клиента и смотрим, увеличилось ли время гидрейта или инита. Они дают понять, была ли проблема с клиентом или нужно смотреть на сеть, SSR и бэкенды.
  2. Триггеры позволяют понимать, когда были проблемы во время релизов. На моей памяти это было лишь однажды. Статика крайне редко ломает релизы настолько сильно.

LongTasks


PerformanceObserver позволяет трекать тяжелые таски у пользователей:


История появления данного графика занятна:


Весна, поют птички, приходят разработчики в офис (да, это не 2020!). Прилетает сообщение от техподдержки: сайт не работает! Разработчики быстро просыпаются и пытаются воспроизвести проблему. Количество обращений растет.


Выясняется довольно занятная штука: поставщик рекламы добавил новый баннер с кормом для собак, где блокирующий js постоянно вызывал reflow, который надежно убивал event loop ровно на 30 секунд.


В общем, владельцы собак пострадали. К сожалению, в то время в команде архитектуры владельцев собак не было. Сейчас не проверяли, не довелось ;)


Из этой истории мы вынесли 2 урока:


  1. Решить, что сделать с рекламой
  2. Трекать Longtasks. Сказано сделано.

Что еще?


Это не все графики, которые мы строим для клиента. У нас есть время инициализации не реакт-компонентов, на старых страницах, rate ошибок в сентри + триггер, чтобы быстро реагировать при проблемах, FID. Но они практически не использовались нами в аналитике.


С клиентом разобрались, переходим к серверу.


Сервер и кулстори


Здесь все намного проще. Сервер это наше игровое поле. Мы его контролируем, мы знаем окружение и что на него может повлиять.


Что самое важное для серверов? Нагрузка, время ответа и понимание на что это время потратили.


У нас огромное количество графиков, которые трекают память, CPU, диски массу всего. Остановимся на специфичном и наиболее часто используемом нашей командой для SSR. Вот так выглядят наши серверные графики:


График запросов и ошибок


График времени ответа http клиента


Каждая линия отдельный урл, здесь TOP наиболее проблемных урлов. Все триггеры настроены на 95 перцентиль. На графике мы видим, что был некий всплеск в 12:10 и затем одному урлу стало не очень хорошо в 12:40. На этих графиках "криминала нет", но как только потолок в 400мс пробивается, в это время зажигается триггер и один человек из команды бодро марширует во внутренние сервисы с логами, кибану и разбирает "что это было". Также локализовать проблему помогают дополнительные графики:


Время рендера и парcинг



Здесь уже видно, что первая проблема коррелирует с увеличением parse time.


Копаем дальше и видим график утилизации CPU. Здесь дискотека:



Это поведение характерно, когда происходит релиз сервиса. Каждый сервис у нас релизится как минимум 1 раз в день. Обычно чаще.


Причина первого скачка на графике становится довольно понятной: у нас был релиз. В 12 часов нагрузка на сервис достаточно высокая, поэтому на часть инстансов прилетело больше запросов. Но все равно это порядка 150мс, до 400мс еще далеко.


Бывает другая ситуация. Собственно тот случай и сподвиг меня написать эту статью. Пятница, утро, вместо кофе видишь в slack зажегшийся триггер:



Одному из урлов надоело жить.


В то же время render и parse time чувствуют себя отлично:



На графике видно, что количество ошибок увеличивается:



Грепаем логи, из них извлекаем ошибку


TypeError: Cannot read property 'map' of undefined    at Social (at path/to/module)

Кажется, сервер стал асоциальным.


Проблема локализована, хотфикс выпущен, графики стабилизируются, кофе остыл:



И еще один пример, когда parse time имеет значение:



Видим постепенно растущий график времени ответа сервиса. Но время рендера совсем не растет. А время парсинга наоборот крайне подозрительно коррелирует с временем ответа:



У нас SSR работает as a service. То есть у нас BFF, которая ходит в наш node.js сервис, для рендера данных. Сама BFF написана на питоне.


Подобная корреляция между временем ответа, парсингом и полным отсутствием влияния на время рендера возможна, как мне кажется, только в одном случае: BFF посылает с каждым разом все больше и больше данных, которые никак не используются node.js. По простому BFF дала течь. Сервис правится, ситуация разруливается.


Сама протечка получилась небольшой и на графиках используемой памяти в BFF это было практически незаметно. А вот на времени ответов \ парсинге это сказалось отрицательно.


Мораль


Сей басни такова:


На сервере достаточно легко вычислять корреляции и предпринимать правильные шаги. Это мы разобрали на примерах.


Чем больше информации вы трекаете, тем проще понимать что происходит.


С первого взгляда, на клиенте должно работать тоже самое. Но там все сильно сложнее. Чего стоит только пример с FMP. Кроме этого почти все клиенские графики, имеют выбросы, достаточно сложно поддаются аналитике, мы не знаем всех условий, в котором находится наш пользователь. Однако аналитика по-прежнему возможна. Мы можем локализовать проблему: загрузка, инит до гидрейта, рендер, реклама и\или аналитика, лонгтаски.


Все это позволяет нам обрести глаза и своевременно реагировать на проблемы.

Подробнее..

Моментальная загрузка с instant.page

18.09.2020 12:15:01 | Автор: admin


instant.page это небольшой скрипт, позволяющий ускорять навигацию по сайту с помощью just-in-time предзагрузки. Когда пользователь наводит курсор на ссылку, страница предзагружается в фоне, и при переходе по ссылке открывается моментально. По тому же принципу работает InstantClick, но он предоставляется уже как отдельная библиотека на pushState и Ajax, с дополнительными модулями вроде прогресс-бара предзагрузки.

Установка


Просто добавьте скрипт в конец body:

<script src="http://personeltest.ru/aways/instant.page/5.1.0" type="module" integrity="sha384-by67kQnR+pyfy8yWP4kPO12fHKRLHZPfEsiSXR8u2IKcTdxD805MGUXBzVPnkLHw"></script>

Может показаться, что в современном вебе бессмысленно распространять своё решение через скрипт вместо размещения в npm, но это не JQuery и весь код буквально умещается в чуть больше 200 строк, поэтому подключить его может себе позволить даже самое легковесное приложение. К тому же, в виде модуля уже есть сверхпопулярный но спорный quicklink от Google.

Десктоп


У разных исследований получаются разные значения, поэтому нельзя точно сказать, с какой вероятностью человек нажмёт на ссылку, на которую он навёл курсор, и как быстро это произойдёт. Автор скрипта утверждает, что если указатель уже наведён на ссылку более 65 миллисекунд, пользователь в половине случае перейдёт по ней.

Для понижения количества ложных срабатываний и в instant.page, и в InstantClick предлагают опционально триггерить предзагрузку в момент нажатия (mousedown), что в среднем должно ускорить загрузку на 80 миллисекунд. Правда, скрипт загружает только HTML, а другие жирные ресурсы всё равно будут загружаться в обычном режиме.

В instant.page есть и совсем безумный триггер для маньяков (и любителей quicklink), загружающий страницу, когда ссылка попадает в зону видимости. К счастью, в противовес ему есть и режим белого списка.

Телефон


В мобильной версии нет ховера, поэтому предзагрузка начинается в момент начала нажатия на ссылку, что даёт прирост около 90 миллисекунд, либо также можно выбрать триггер по попаданию в viewport, но для этого надо иметь или очень тонкие страницы, или минимальное и известное заранее количество ссылок (например, можно предзагрузить следующую страницу после отправки формы), или настроить белый список.

Настройка


  • Белый список: предзагрузка работает только для ссылок с атрибутом data-instant (для этого в body надо добавить атрибут data-instant-whitelist)
  • Чёрный список: не будут загружаться ссылки с атрибутом data-no-instant
  • Внешние ссылки по умолчанию не загружаются, изменить это можно, добавив data-instant-allow-external-links в body
  • Ссылки с вопросительным знаком по умолчанию не загружаются, потому что могут вызывать нежелательные действия. Чтобы разрешить их, нужно добавить data-instant-allow-query-string в body


Проблемы


  • uBlock Origin и другие блокировщики, использующие список правил EasyPrivacy, блокируют скрипт как потенциальную угрозу приватности. Автор пытался убедить его мейтейнеров убрать instant.page из списка, но в итоге ему отказали и закрыли issue. При использовании скрипта на своём сайте можно просто захостить его у себя, что позволит обойти подобные блокировки. Однако у пользователей на Firefox с включённым uBlock Origin вырезается вообще любая предзагрузка, поэтому для них это решение не поможет.
  • В Safari 13 поддержка выключена по умолчанию. Должно быть исправлено в Safari 14.


Заключение


Меньше чем за год (а популярность пришла к instant.page даже меньше полугода назад) технология закрепилась на рынке, обзавелась крупными клиентами вроде Spotify и Pepsico и подбирается по количеству звёзд на GitHub к InstantClick, который там находится с 2014 года. Автор заявляет (со ссылкой на builtwith) о более чем 7000 сайтов, использующих instant.page, с суммарной аудиторией более 76 миллионов пользователей ежемесячно.



На правах рекламы


VDS для сайтов любых масштабов это про наши эпичные серверы! Они бесплатно защищены от DDoS-атак, скорость интернет-канала 500 Мегабит. Предоставляем возможность автоматически установить удобную панель управления VestaCP для размещения сайтов. Поспешите заказать!

Подробнее..

Делаем схему выбора мест в кинозале на React о canvas, красивом дизайне и оптимизации

25.12.2020 18:10:30 | Автор: admin

В богатой экосистеме Тинькофф есть лайфстайл-сервисы. Купить билеты на различные мероприятия - в кино, театры, на концерты, спортивные события можно на https://www.tinkoff.ru/entertainment/, а также в мобильном приложении.

Меня зовут Вадим и я расскажу вам, как мы это делали в команде Развлечений в Тинькофф Банке.


Что нужно, чтобы купить билет в кино?

Когда вы определились с фильмом и кинотеатром, вам нужно выбрать место в кинозале - изучить красивую схему выбора мест и купить самые лучшие билеты.

дизайн схемы выбора местдизайн схемы выбора мест

Мы придумали три варианта реализации такой схемы.

1. Сделать все на старом добром HTML

Схема на HTML. Найдено на просторах ИнтернетаСхема на HTML. Найдено на просторах Интернета

Плюсы:

  • Удобно стилизовать.

  • Удобно работать в React.

  • Все доступно (A11Y).

Минусы:

  • Растет количество DOM-нод и глубина DOM-дерева (пример на изображении выше).

  • Проблемы с производительностью при взаимодействии с пользователем (перемещение схемы).

2. Использовать SVG

Плюсы и минусы примерно такие же, как и с HTML.

Получилось найти только схему метро на SVGПолучилось найти только схему метро на SVG

3. Canvas

Стильно. Ярко. Производительно.Стильно. Ярко. Производительно.

Плюсы:

  • Удобно стилизовать (можно нарисовать что угодно).

  • Меньше проблем с производительностью.

Минусы:

  • Не получится совместить с Server Side Rendering.

  • Проблемы с A11Y (нет из коробки).

Мы решили делать схему на canvas, потому что нам важно, чтобы все было красиво и с приятным UX для пользователя. Также с технической стороны у нас пропадают проблемы с глубиной DOM-дерева и количеством нод в нем. Тем более что canvas без проблем работает даже в Internet Explorer 11.

Конечно, на наше решение повлияло и то, что использовать canvas намного интереснее, чем просто работать с SVG- и HTML-решениями.

Экосистема вокруг canvas

Итак, мы отправились выбирать библиотеку для более удобной работы с canvas. Как оказалось, их существует достаточно большое количество, из самых популярных Konva, PixiJS, Fabric.js и Phaser.

Из этого многообразия мы выбрали PixiJS. Ключевые особенности Pixi: он быстрый, гибкий и производительный. Также наши коллеги активно рекомендовали использовать именно его.

Простой код на PixiJS. Мы инстанцируем Pixi.Appс заданным конфигом (например, ширину, высоту, цвет фона, разрешение). Добавляем объекты на сцену (Stage в терминологии Pixi), пишем простой цикл и получаем сетку 5 5 из кроликов, которые вращаются вокруг своей оси пример с официального сайта Pixi

Структура и читаемость

Этот код достаточно простой для понимания, но и делает он не так уж много. Если мы хотим создать что-то наподобие схемы выбора мест, код становится достаточно объемным и плохо воспринимается.

Выше не слишком крупная программа, но мы уже дошли до 100 строк кода и, просто глядя на этот код, тяжело понять, что же происходит.

Вписываем в React

Кроме сложности понимания кода возникает другой вопрос: как это вписать в парадигму React?

Сначала мы решили сделать свои обертки для разных примитивов. Но впоследствии обнаружили, что за нас все уже придумали.

Одно из решений, которое понравилось нам, библиотека react-pixi-fiber. Ее плюс в том, что мы пишем привычный нам JSX, а под капотом происходит взаимодействие с Pixi и мы получаем наш canvas.

В этой библиотеке у нас уже есть обертки для всех нативных объектов Pixi. К примеру, вместо инстанцирования класса Pixi.Textмы используем react-элемент <Text />.

Также есть удобное АПИ для создания своих объектов CustomPIXIComponent

Приблизительно так теперь выглядит код для нашей схемы выбора мест. Здесь уже нет никаких инстансов Pixi, у нас обычный JSX: компоненты Stage, Container, посадочные места, привычный маппинг данных на react-компоненты.

А вот как выглядит создание своего компонента. Он немного отличается от привычных react-компонентов, но, если разобраться, по сути тут все то же самое. У нас есть ссылка на отображаемый компонент graphics и привычное слово props. Также почти привычным образом мы можем использовать обработчики событий, например ховер, клик и так далее.

Применяем все на практике

Какие у нас были вводные для отрисовки кресел?

У нас была информация в виде массива объектов. В каждом данные, необходимые для отрисовки сиденья: размеры, координаты, номер места и ряда.

Наша задача сделать так, чтобы в любых сочетаниях кресла смотрелись красиво.

В зависимости от ценовой категории кресло может иметь различный цвет. А также кресла могут быть разных размеров: например, это сдвоенные места диванчики.

Вариант с загрузкой кресла как простой текстуры мы сразу отбросили: были проблемы с отображением на retina-экранах и в целом с изменением размеров без визуальной деформации. А с SVG в то время были проблемы у PixiJS: некорректно работала подгрузка ассетов в SVG.

Поэтому мы решили сами рисовать каждое кресло.

Рисуем кресло на PixiJS

Для удобства мы разделили кресло на сектора:

A полукруглые края подлокотников.
B подлокотник.
C кривая от подлокотников до спинки кресла.
D спинка кресла.
E верхняя часть кресла.
F средняя часть кресла.
G нижняя часть кресла.

Ширина одной клетки width / 22.
Высота одной клетки height / 16.
Кресло в макете у нас имеет размер 22 пикселя на 16, таким образом, каждая черточка или буковка это пиксель в сетке.

Затем мы разделили эту сетку на зоны: подлокотники, спинка и так далее. И отрисовали все по частям, используя PixiJS и CustomPIXIComponent.

Теперь все по порядку и каждому разработчику, который приходит в этот код, сразу понятно, где, как и что.

Мы решили все задачи: кресла могут быть любых размеров без потери пропорций, реагируют на действия пользователя и выглядят круто!

Схемы секторов

Когда вы покупаете билет на крупное мероприятие, например на хоккей или на концерт в Олимпийском, скорее всего, сначала вы выбираете сектор, в котором хотите сидеть, а потом уже места в этом секторе. С появлением задач по концертам нам тоже нужно было это реализовать.

От наших партнеров приходила такая схема секторов.
Собственно массив секторов в поле sectors с информацией о каждом секторе, название площадки, а также строка hallScheme, которая занимает почти 236 килобайт.

Как оказалось, это схема секторов площадки в SVG и закодирована в base64.

Что же нам с этим делать?

Первым нашим решением было парсить этот SVG и как-то перевести на PixiJS.

Второй вариант просто вставить это как HTML, повесить обработчики через стандартные методы.

Рассмотрев эти варианты и взвесив плюсы и минусы, мы решили пойти дальше и сделать третий вариант парсить эту SVG и превращать ее в react-элементы.

Выбором для парсера стал html-react-parser. Эта библиотека парсит любой валидный HTML в react-элементы. Работает как на стороне Node.js, так и на стороне браузера. Но решающим стало то, что любой элемент из оригинальной разметки можно заменить на что угодно.

Передаем всю разметку схемы в функцию parseHtmlToReact а также через опции задаем функцию, которая будет заменять элементы на наши.

Здесь уже опять привычный нам JSX и полный контроль над всем, что нужно: обработчики событий, стилизация и так далее.

Вот так теперь выглядит ВТБ Арена.Вот так теперь выглядит ВТБ Арена.

Поговорим об оптимизации

Во время работы над схемами мы заметили, что даже для маленьких схем загрузка процессора держится на 20%, а в больших достигает 8090%. Визуально это незаметно для пользователя, но может привести к проблемам на слабых мобильных устройствах и быстрому разряду батареи.

Используя инструменты разработчика, мы видим, что даже при простое приблизительно каждые 16 мс вызывается одна и та же таска. Сразу видно некий Ticker_tick

В замечательной документации по Pixi можно найти упоминание об этом тикере. Как понятно из описания, это некий цикл, который выполняет что-то за некий интервал времени, в нашем случае приблизительно каждые 16 мс.

Но почему именно 16 миллисекунд?

Вспомним понятие 60 кадров в секунду - это нужно, чтобы обеспечить плавность анимации и перемещений. Также, новые фильмы снимают в 60 кадров в секунду, что дает более плавную картинку.

Чтобы получить такую частоту обновления, нужно каждую секунду обновлять изображение 60 раз: 1000 мс 60 = 16,6666 мс.

Как раз этот цикл из класса Pixi.Tickerобеспечивает обновление 60 раз в секунду всего canvas, и у нас все плавно и красиво. В нашем случае при большом количестве объектов перерисовка выходит достаточно дорогой. При этом чаще всего схема абсолютно статичная, а плавность нужна только при взаимодействии.

Так как мы не работаем напрямую с Pixi, получить доступ к регулированию цикла обновления мы не могли.

Исходный код компонента Stage из react-pixi-fiberИсходный код компонента Stage из react-pixi-fiber

Как видно, вся работа с Pixi происходит внутри компонента Stage от react-pixi-library. К сожалению, официальных способов от создателей react-pixi-library по работе с Ticker нет.

В нашем случае выходом стало применение опции sharedTickerдля Pixi. По сути, эта опция включает использование всех инстансов pixi-приложений общего Ticker. Общий Ticker доступен простым импортом из пакета.

Мы сразу отключили автоматический старт цикл обновлений Ticker с инициализацией приложения. А дальше мы связали это с ререндером react-компонента, так как при взаимодействии пользователя со схемой меняются props данного компонента.

Соответственно, цикл обновления запускается, только когда нужно. В остальных случаях canvas статичен, выглядит как картинка и не нагружает ресурсы.

Пока мы все это изучали, обнаружили, что у Pixi на Github есть целая wiki, где очень много интересной информации:

Забавно, что на оффициальном сайте Pixi ссылку на эту wiki не найти.

Главный совет по оптимизации заключается в том, что инстансы объектов Pixi.Graphics стоят дорого и не кэшируются, в отличие от текстур, спрайтов и так далее. А наши кресла, как сложные объекты, как раз и являются инстансами Pixi.Graphics.

Выводы

Какие выводы из этого всего можно сделать?

  1. Чем меньше оберток тем более гибко мы можем оптимизировать приложение.

  2. Работа с canvas отличается от обычных рутинных задач.

  3. Pixi заточен под более интерактивные вещи, например игры.

  4. При разработке желательно сразу иметь большой объем данных. Например, в нашем случае надо было сразу получить схему какого-нибудь огромного концертного зала вместо зала кинотеатра.

Подробнее..

Iresine, нормализация данных на клиенте

17.03.2021 14:12:14 | Автор: admin

Нормализация. От нее мы или страдаем или пишем собственное решение с множеством проверок на существование сущности в общем хранилище. Попробуем разобраться и решить эту проблему!

Описание проблемы

Представим себе такую последовательность:

  1. Клиентское приложение запрашивает список пользователей запросом к /users и получается пользователей с id от 1 до 10

  2. Пользователь с id 3 меняет свое имя

  3. Клиентское приложение запрашивает пользователя с id 3 с помощью запроса к /user/3

Вопрос:Какое имя пользователя с id 3 будет в приложении?
Ответ:Зависит от компонента, который запросил данные. В компоненте, который использует данные из запроса к /users, будет отображаться старое имя. В компоненте, который использует данные из запроса к /user/3, будет отображаться новое имя.

Вывод: В таком случае в системе существует несколько одинаковых по смыслу сущностей с разным набором данных.

Вопрос:Почему это плохо?
Ответ:В лучшем случае пользователь увидит разные имена одного человека в разных разделах сайта, в худшем переведет деньги на старые банковские реквизиты.

Варианты решения

В настоящее время существуют следующие варианты решения этой проблемы:

  • Не обращать внимание

  • Нормализовать данные собственноручно

  • Использовать клиент graphql (apollo или relay)

Не обращать внимание

Это самый очевидный и заманчивый вариант. В некоторых случаях клиентское приложение действительно может позволить себе иметь одинаковые сущности с разными данными. Но что делать со случаями, когда это недопустимое поведение? Как быть с разработчиками, которые не хотят создавать приложение с такими дефектами?

Нормализовать данные собственноручно

Примером собственноручной реализации может послужить код для mobx:

class Store {  users = new Map();  async getUsers() {    const users = await fetch(`/users`);    users.forEach((user) => this.users.set(user.id, user));  }  async getUser(id) {    const user = await fetch(`/user/${id}`);    this.users.set(user.id, user);  }}

И если пример с mobx выглядит приемлемо, то нормализация в redux простоужасает. Работать с таким кодом становится сложнее по мере его увеличения и совсем неинтересно

Использовать клиент graphql (apollo или relay)

Apollo и relay это библиотеки, которые из коробки умеют нормализовать данные. Однако такое решение заставляет нас использовать graphql и apollo, которые, по моему мнению, имеют множество недостатков.

Нормализация

Что такое нормализация и как она позволяет graphql клиентам бороться с указанной проблемой? Разберемся на примере apollo! Так apollo описывает свои действия с данными:

...normalizesquery response objects before it saves them to its internal data store.

Что включает в себя указанноеnormalize?

Normalization involves the following steps:

1. The cache generates a unique ID for every identifiable object included in the response.
2. The cache stores the objects by ID in a flat lookup table.

То есть apollo формирует уникальный идентификатор для каждой сущности, для которой возможно его сформировать. Apollo использует его как ключ в хранилище всех сущностей. Вот как примерно выглядит формирование идентификатора и его хранение:

const store = new Map();const user = {  id: '0',  type: 'user',  name: 'alex',  age: 24,};const id = `${user.type}:${user.id}`;store.set(id, user);

Комбинация типа и id дает нам по-настоящему уникальный ключ. Мы можем быть уверены, что если встретим другого пользователя с таким же типом и id, то это будет тот же пользователь.

Получение уникального идентификатора

Apollo достигает указанного эффекта, запрашивая при каждом запросе внутреннее поле __typename, а как достигнуть похожего эффекта без graphql?

Поскольку мы не имеем внутренних полей с типами, то должны полагаться только на поля данных. Вот несколько решений:

  • сделать поле id или аналогичное поле глобально уникальным

  • добавить информацию о типах сущности в данные

    • добавить типы на сервере

    • добавить типы на клиенте

Сделать поле глобально уникальным

В таком случае хранение сущностей будет выглядеть вот так:

const store = new Map();const user = {  id: '0',};const comment = {  id: '1',};store.set(user.id, user);store.set(comment.id, comment);// ...store.get('0'); // userstore.get('1'); // comment

Решение выглядит достаточно удобным в использовании, однако реализация глобально уникальных полей id будет затруднительна. Как правило, сущности хранятся в базе данных и имеют id уникальный только внутри коллекции/таблицы (или другими словами какого-то типа). А значит, чтобы сделать id глобально уникальным, нужно приложить много усилий.

Добавить информацию о типах

В таком случае хранение сущностей выглядеть вот так:

const store = new Map();const user = {  id: '0',  type: 'user', // <-- new field};const comment = {  id: '1',  type: 'comment', // <-- new field};function getStoreId(entity) {  return `${entity.type}:${entity.id}`;}store.set(getStoreId(user), user);store.set(getStoreId(comment), comment);// ...store.get('user:0'); // userstore.get('comment:1'); // comment

По-прежнему удобно, но при этом требует от нас добавления особого поля в данных. Как мне кажется эта небольшая жертва окупается возможностью автоматического отслеживания изменения в данных. Именно этот вариант я выбрал предпочтительным для себя.

Где добавлять типы в данные?

Проблема нормализации данных особенно характерна для клиентских приложений. Поэтому рассмотрим вопрос - в какой момент добавлять информацию о типах в данные. Мы можем выбрать один из указанных вариантов для добавления типов.

  • На сервере, при отдаче данных:

app.get('/users', (req, res) => {  const users = db.get('users');  const typedUsers = users.map((user) => ({    ...user,    type: 'user',  }));  res.json(typedUsers);});
  • На клиенте, при получении данных:

function getUsers() {  const users = fetch('/users');  const typedUsers = users.map((user) => ({    ...user,    type: 'user',  }));  return typedUsers;}

Как мне кажется вариант добавления данных на сервере является предпочтительным. Api, которое отдает данные, знает о том какие данные и какого типа отдает. Однако в некоторых случаях нет возможности изменить код сервера для отдачи типа, в таких случаях можно добавить типы на клиенте.

Теперь разберемся как все это автоматизировать.

iresine

iresineэто библиотека созданная для нормализации данных и оповещении об их изменении.

В данный момент iresine состоит из следующих модулей:

Так iresine работает с react-query:

@iresine/core

Основной модуль библиотеки, именно он отвечает за парсинг данных, их нормализацию и оповещении подписчиков об изменении конкретной сущности.

const iresine = new Iresine();const oldRequest = {  users: [oldUser],  comments: {    0: oldComment,  },};// new request data have new structure, but it is OK to iresineconst newRequest = {  users: {    0: newUser,  },  comments: [newComment],};iresine.parse(oldRequest);iresine.parse(newRequest);iresine.get('user:0' /*identifier for old and new user*/) === newRequest.users['0']; // trueiresine.get('comment:0' /*identifier for old and new comment*/) === newRequest.comments['0']; // true

Как видим из идентификаторов, по которым мы получаем сущности из хранилища, @iresine/core использует следующую схему для создания идентификаторов:

entityType + ':' + entityId;

По умолчанию @iresine/core берет тип из поляtype, а id из поляid. Это поведение можно изменить, передав собственные функции. Например попробуем использовать такой же идентификатор как в apollo:

const iresine = new Iresine({  getId: (entity) => {    if (!entity) {      return null;    }    if (!entity.id) {      return null;    }    if (!entity.__typename) {      return null;    }    return `${entity.__typename}:${entity.id}`;  },});

Так же мы можем обрабатывать и глобально уникальное поле id:

const iresine = new Iresine({  getId: (entity) => {    if (!entity) {      return null;    }    if (!entity.id) {      return null;    }    return entity.id;  },});

А что @iresine/core делает с сущностями, где идентификатор не обнаружен? Например такими:

const user = {  id: '0',  type: 'user',  jobs: [    {      name: 'milkman',      salary: '1$',    },    {      name: 'woodcutter',      salary: '2$',    },  ],};

user имеет своей идентификатор в хранилище, а как быть с jobs? У них нет ни поля type ни поля id! @iresine/core следует простому правилу: если у сущности нет идентификатора, то она становится частью ближайшей родительской сущности с идентификатором.

@iresine/core являет универсальной библиотекой, которая знает о том как распарсить данные и точечно уведомлять подписчиков. Но использовать ее напрямую довольно нудно и утомительно! Посмотрим как сделать этот процесс удобнее.

@iresine/react-query

react-query это прекрасная библиотека, с которой я бы посоветовал ознакомиться каждому. Но в ней отсутствует нормализация данных, и именно этот факт вдохновил меня на написание iresine.

@iresine/react-query это плагин для react-query. Он позволяет использовать функцию нормализации и обновления данных @iresine/core на данных хранилища react-query. Вся работа по нормализации происходит автоматически и клиент работает с react-query так, как бы работал без iresine.

import Iresine from '@iresine/core';import IresineReactQuery from '@iresone/react-query';import {QueryClient} from 'react-query';const iresineStore = new IresineStore();const queryClient = new QueryClient();new IresineReactQueryWrapper(iresineStore, queryClient);// now any updates in react-query store will be consumbed by @iresine/core

Схема взаимодействия выглядит так(была приведена выше):

Итог

Нормализация данных на клиенте это проблема. Сейчас она решается разными способами с разной степенью успешности. В написанном выше материале автор предлагает свой способ решения этой проблемы. Если сократить все предложение до нескольких слов, то они будут звучать какдобавьте информацию о типах в данные, а после этого используйтеiresine

Подробнее..

Кэширование данных увеличивает скорость даже в неожиданных случаях

13.04.2021 16:13:10 | Автор: admin

Нас учат, что чтение данных из оперативной памяти ужасно долгая операция. Приводят аналогии с офисом и удалённым складом, заставляют писать cache-friendly код и внушают смертельный страх перед промахами кэша. Ещё нас учат, что процессоры отлично умеют считать числа, и часто быстрее вычислить результат дважды, чем сохранять его в памяти. Оказывается, это не всегда так.

Эта статья основана на реальном проекте и реальном коде, который был ускорен с помощью кэша почти в полтора раза. Весь код написан на JavaScript.

Задача

Допустим, у нас есть матрица A порядка 2000x2000. Нужно посчитать обратную ей матрицу по простому модулю N. Другими словами, надо найти такую матрицу A-1, что AA-1 mod N = E.

Поскольку вычисления у нас происходят в поле по модулю, итерационные методы обращения нам не подойдут. Будем использовать старый добрый метод Гаусса.

Этот пост посвящён оптимизации метода Гаусса под данный конкретный случай. В реальном проекте вычисление обратной матрицы происходит в отдельном WebWorker, в данном примере обойдёмся главным потоком.

Вспомогательные функции

Для работы программы нам потребуется четыре вспомогательные функции. Первая вычисление (1 / x) mod N по расширенному алгоритму Евклида:

function invModGcdEx(x, domain){    if(x === 1)    {        return 1;    }    else    {        //В случае 0 или делителя нуля возвращается 0, означающий некий "некорректный результат"        if(x === 0 || domain % x === 0)        {            return 0;        }        else        {            //Расширенный алгоритм Евклида, вычисляющий такое число tCurr, что tCurr * x + rCurr * N = 1            //Другими словами, существует такое число rCurr, при котором tCurr * x mod N = 1            let tCurr = 0;            let rCurr = domain;            let tNext = 1;            let rNext = x;            while(rNext !== 0)            {                let quotR = Math.floor(rCurr / rNext);                let tPrev = tCurr;                let rPrev = rCurr;                tCurr = tNext;                rCurr = rNext;                tNext = Math.floor(tPrev - quotR * tCurr);                rNext = Math.floor(rPrev - quotR * rCurr);            }            tCurr = (tCurr + domain) % domain;            return tCurr;        }    }}

Вторая корректное целочисленное деление по модулю. Наивное вычисление c = a % b во всех языках программирования не будет давать математически верный результат, если a отрицательное число. Поэтому заведём функцию, которая будет делить правильно:

function wholeMod(x, domain){    return ((x % domain) + domain) % domain;}

Последние две функции относятся к операциям над строками матрицы. Первая вычитание из строки матрицы домноженной на число другой:

function mulSubRow(rowLeft, rowRight, mulValue, domain){    for(let i = 0; i < rowLeft.length; i++)    {        rowLeft[i] = wholeMod(rowLeft[i] - mulValue * rowRight[i], domain);    }}

Последняя нужная нам функция умножение строки матрицы на число:

function mulRow(row, mulValue, domain){    for(let i = 0; i < row.length; i++)    {        row[i] = (row[i] * mulValue) % domain;    }}

Обращение матрицы

Начнём с обычной наивной реализации. Создаём единичную матрицу, проходим прямым ходом, потом проходим обратным. На каждом шаге производим одинаковые операции над исходной матрицей и над только что созданной единичной.

function invertMatrix(matrix, domain){    let matrixSize = matrix.length;    //Инициализируем обратную матрицу единичной    let invMatrix = [];    for(let i = 0; i < matrixSize; i++)    {        let matrixRow = new Uint8Array(matrixSize);        matrixRow.fill(0);        matrixRow[i] = 1;        invMatrix.push(matrixRow);    }    //Прямой ход: приведение матрицы к ступенчатому виду    for(let i = 0; i < matrixSize; i++)    {        let thisRowFirst = matrix[i][i];        if(thisRowFirst === 0 || (thisRowFirst !== 1 && domain % thisRowFirst === 0)) //Первый элемент строки 0 или делитель нуля, меняем строку местами со следующей строкой, у которой первый элемент не 0        {            for(let j = i + 1; j < matrixSize; j++)            {                let otherRowFirst = matrix[j][i];                if(otherRowFirst !== 0 && (otherRowFirst === 1 || domain % otherRowFirst !== 0)) //Нашли строку с ненулевым первым элементом                {                    thisRowFirst = otherRowFirst;                                        let tmpMatrixRow = matrix[i];                    matrix[i]        = matrix[j];                    matrix[j]        = tmpMatrixRow;                    let tmpInvMatrixRow = invMatrix[i];                    invMatrix[i]        = invMatrix[j];                    invMatrix[j]        = tmpInvMatrixRow;                    break;                }            }        }        //Обнуляем первые элементы всех строк после первой, отнимая от них (otherRowFirst / thisRowFirst) * x mod N        let invThisRowFirst = invModGcdEx(thisRowFirst, domain);        for(let j = i + 1; j < matrixSize; j++)        {            let otherRowFirst = matrix[j][i];            let mulValue      = invThisRowFirst * otherRowFirst;            if(otherRowFirst !== 0 && (otherRowFirst === 1 || domain % otherRowFirst !== 0))            {                mulSubRow(matrix[j],    matrix[i],    mulValue, domain);                mulSubRow(invMatrix[j], invMatrix[i], mulValue, domain);            }        }    }    //Обратный ход - обнуление всех элементов выше главной диагонали    let matrixRank = matrixSize;    for(let i = matrixSize - 1; i >= 0; i--)    {        let thisRowLast    = matrix[i][i];        let invThisRowLast = invModGcdEx(thisRowLast, domain);        for(let j = i - 1; j >= 0; j--)        {            let otherRowLast = matrix[j][i];            let mulValue     = invThisRowLast * otherRowLast;            if(otherRowLast !== 0 && (otherRowLast === 1 || domain % otherRowLast !== 0))            {                mulSubRow(matrix[j],    matrix[i],    mulValue, domain);                mulSubRow(invMatrix[j], invMatrix[i], mulValue, domain);            }        }        if(thisRowLast !== 0 && domain % thisRowLast !== 0)        {            mulRow(matrix[i],    invThisRowLast, domain);            mulRow(invMatrix[i], invThisRowLast, domain);        }        if(matrix[i].every(val => val === 0))        {            matrixRank -= 1;        }    }    return {inverse: invMatrix, rank: matrixRank};}

Проверим скорость на матрице 500 x 500, заполненной случайными значениями из поля Z / 29. После 5 испытаний получаем среднее время выполнения в ~9.4с. Можем ли мы сделать лучше?

Первое, что мы можем заметить в поле Z / N не больше N обратных элементов. Чтобы избежать многократного вызова алгоритма Евклида, мы можем вычислить все обратные значения один раз и при надобности брать уже готовые. Изменим функцию соответствующим образом:

function invertMatrixCachedInverses(matrix, domain){    let matrixSize = matrix.length;    //Инициализируем обратную матрицу единичной    let invMatrix = [];    for(let i = 0; i < matrixSize; i++)    {        let matrixRow = new Uint8Array(matrixSize);        matrixRow.fill(0);        matrixRow[i] = 1;        invMatrix.push(matrixRow);    }    //Вычисляем все обратные элементы заранее    let domainInvs = [];    for(let d = 0; d < domain; d++)    {        domainInvs.push(invModGcdEx(d, domain));    }    //Прямой ход: приведение матрицы к ступенчатому виду    for(let i = 0; i < matrixSize; i++)    {        let thisRowFirst = matrix[i][i];        if(domainInvs[thisRowFirst] === 0) // <--- Первый элемент строки 0 или делитель нуля, меняем строку местами со следующей строкой, у которой первый элемент не 0        {            for(let j = i + 1; j < matrixSize; j++)            {                let otherRowFirst = matrix[j][i];                if(domainInvs[otherRowFirst] !== 0) // <--- Нашли строку с ненулевым первым элементом                {                    thisRowFirst = otherRowFirst;                                        let tmpMatrixRow = matrix[i];                    matrix[i]        = matrix[j];                    matrix[j]        = tmpMatrixRow;                    let tmpInvMatrixRow = invMatrix[i];                    invMatrix[i]        = invMatrix[j];                    invMatrix[j]        = tmpInvMatrixRow;                    break;                }            }        }        //Обнуляем первые элементы всех строк после первой, отнимая от них (otherRowFirst / thisRowFirst) * x mod N        let invThisRowFirst = domainInvs[thisRowFirst]; // <---        for(let j = i + 1; j < matrixSize; j++)        {            let otherRowFirst = matrix[j][i];            let mulValue      = invThisRowFirst * otherRowFirst;            if(domainInvs[otherRowFirst] !== 0) // <---            {                mulSubRow(matrix[j],    matrix[i],    mulValue, domain);                mulSubRow(invMatrix[j], invMatrix[i], mulValue, domain);            }        }    }    //Обратный ход - обнуление всех элементов выше главной диагонали    let matrixRank = matrixSize;    for(let i = matrixSize - 1; i >= 0; i--)    {        let thisRowLast    = matrix[i][i];        let invThisRowLast = domainInvs[thisRowLast]; // <---        for(let j = i - 1; j >= 0; j--)        {            let otherRowLast = matrix[j][i];            let mulValue     = invThisRowLast * otherRowLast;            if(domainInvs[otherRowLast] !== 0) // <---            {                mulSubRow(matrix[j],    matrix[i],    mulValue, domain);                mulSubRow(invMatrix[j], invMatrix[i], mulValue, domain);            }        }        if(domainInvs[thisRowLast] !== 0) // <---        {            mulRow(matrix[i],    invThisRowLast, domain);            mulRow(invMatrix[i], invThisRowLast, domain);        }        if(matrix[i].every(val => val === 0))        {            matrixRank -= 1;        }    }    return {inverse: invMatrix, rank: matrixRank};}

Замерим на тех же условиях и получаем результат в те же ~9.4с. Прироста нет, потому что даже при относительно долгом вычислении алгоритма Евклида он вычисляется всего один раз для каждой строки матрицы и особого вклада во время не приносит. Замерим производительность и посмотрим, что ещё можно улучшить.

72% времени занимает деление по модулю при сложении строк матрицы! Ну что тут сказать, деление по модулю, пусть и немного модифицированное для отрицательных чисел это элементарная операция и ускорять её некуда. Алгоритм поменять тоже не получится, из чего мы делаем вывод, что дальнейшее улучшение невозможно и статью можно закрывать.

...Или всё же возможно?

Если деление по модулю занимает столько времени, может, все возможные результаты тоже стоит сохранить в кэш? Даже если это не поможет, попытаться всё равно стоит при текущем времени выполнения функция неюзабельна.

Итак, используется wholeMod()только в функции mulSubRow():

rowLeft[i] = wholeMod(rowLeft[i] - mulValue * rowRight[i], domain);

Нам нужно для всех возможных значений x = a - b * c в поле Z / N сохранить результат выражения x mod N. Воспользоваться периодичностью мы не сможем, потому что тогда для вычисления индекса снова придётся использовать деление по модулю. В итоге при 0 <= a, b, c < N получаем N + (N - 1)^2 возможных значений. Много, но деваться некуда.

Из этих значений (N - 1)^2 значений меньше 0. Поскольку отрицательные индексы невозможны, при индексировании значением a - b * c к нему нужно прибавить (N - 1)^2. Тогда функция для сложения строк модифицируется:

function mulSubRowCached(rowLeft, rowRight, mulValue, wholeModCache, cacheIndexOffset){    for(let i = 0; i < rowLeft.length; i++)    {        rowLeft[i] = wholeModCache[rowLeft[i] - mulValue * rowRight[i] + cacheIndexOffset];    }}

Заметим, что эта функция накладывает ограничение на mulValue его значение не может быть больше domain и перед вызовом функции его тоже надо привести в наше поле Z / N. Кроме этого, обычное деление по модулю используется в функции mulRow().

Помимо wholeMod в вычитании строк матриц, используется . Кроме того, появилась вышеуказанная проблема с ограничением mulValue. Во всех этих случаях деление описывается формулой x = (a * b) mod N. Зная, что кэш хранит значения x = (c - a * b) mod N, мы можем вычислить (a * b) mod N, взяв значение кэша при c = 0 и вычтя его из N. Тогда функция для умножения строки на число модифицируется следующим образом:

function mulRowCached(row, mulValue, domain, wholeModCache, cacheIndexOffset){    for(let i = 0; i < row.length; i++)    {        row[i] = domain - wholeModCache[cacheIndexOffset - row[i] * mulValue];    }}

И получаем новое обращение матрицы:

function invertMatrix(matrix, domain){    let matrixSize = matrix.length;    //Инициализируем обратную матрицу единичной    let invMatrix = [];    for(let i = 0; i < matrixSize; i++)    {        let matrixRow = new Uint8Array(matrixSize);        matrixRow.fill(0);        matrixRow[i] = 1;        invMatrix.push(matrixRow);    }    //Вычисляем все обратные элементы заранее    let domainInvs = [];    for(let d = 0; d < domain; d++)    {        domainInvs.push(invModGcdEx(d, domain));    }    //Вычисляем кэш деления по модулю    const сacheIndexOffset = (domain - 1) * (domain - 1);    let wholeModCache = new Uint8Array((domain - 1) * (domain - 1) + domain);     for(let i = 0; i < wholeModCache.length; i++)    {        let divisor      = i - сacheIndexOffset;      //[-domainSizeCacheOffset, domainSize - 1]        wholeModCache[i] = wholeMod(divisor, domain); //Whole mod    }    //Прямой ход: приведение матрицы к ступенчатому виду    for(let i = 0; i < matrixSize; i++)    {        let thisRowFirst = matrix[i][i];        if(domainInvs[thisRowFirst] === 0) //Первый элемент строки 0 или делитель нуля, меняем строку местами со следующей строкой, у которой первый элемент не 0        {            for(let j = i + 1; j < matrixSize; j++)            {                let otherRowFirst = matrix[j][i];                if(domainInvs[thisRowFirst] !== 0) //Нашли строку с ненулевым первым элементом                {                    thisRowFirst = otherRowFirst;                                            //Меняем строки местами                    let tmpMatrixRow = matrix[i];                    matrix[i]        = matrix[j];                    matrix[j]        = tmpMatrixRow;                    let tmpInvMatrixRow = invMatrix[i];                    invMatrix[i]        = invMatrix[j];                    invMatrix[j]        = tmpInvMatrixRow;                    break;                }            }        }        //Обнуляем первые элементы всех строк после первой, отнимая от них (otherRowFirst / thisRowFirst) * x mod N        let invThisRowFirst = domainInvs[thisRowFirst]; // <---        for(let j = i + 1; j < matrixSize; j++)        {            let otherRowFirst = matrix[j][j];            if(domainInvs[otherRowFirst] !== 0)            {                let mulValue = domain - wholeModCache[сacheIndexOffset - otherRowFirst * invThisRowFirst]; // <---                mulSubRowCached(matrix[j],    matrix[i],    mulValue, wholeModCache, сacheIndexOffset); // <---                mulSubRowCached(invMatrix[j], invMatrix[i], mulValue, wholeModCache, сacheIndexOffset); // <---            }        }    }    //Обратный ход - обнуление всех элементов выше главной диагонали    let matrixRank = matrixSize;    for(let i = matrixSize - 1; i >= 0; i--)    {        let thisRowLast    = matrix[i][i];        let invThisRowLast = domainInvs[thisRowLast];        for(let j = i - 1; j >= 0; j--)        {            let otherRowLast = matrix[j][i];            if(domainInvs[otherRowLast] !== 0)            {                let mulValue = domain - wholeModCache[сacheIndexOffset - otherRowLast * invThisRowLast]; // <---                mulSubRowCached(matrix[j],    matrix[i],    mulValue, wholeModCache, сacheIndexOffset); // <---                mulSubRowCached(invMatrix[j], invMatrix[i], mulValue, wholeModCache, сacheIndexOffset); // <---            }        }        if(domainInvs[thisRowLast] !== 0)        {            mulRowCached(matrix[i],    invThisRowLast, domain, wholeModCache, сacheIndexOffset); // <---            mulRowCached(invMatrix[i], invThisRowLast, domain, wholeModCache, сacheIndexOffset); // <---        }        if(matrix[i].every(val => val === 0))        {            matrixRank -= 1;        }    }    return {inverse: invMatrix, rank: matrixRank};}

Замерим производительность. На той же матрице 500x500 по модулю 29 получаем время выполнения в ~5.4с.

Простите, что?

Нет, серьёзно, как это возможно? Кэшируем результат деления. Операции на два такта. В век супермедленной памяти и супербыстрых процессоров. Получаем прирост в 40%. Как?

Да, использование JavaScript создаёт определённый оверхед. Но JIT его нивелирует. Видимо, либо он нивелирует его недостаточно, либо не всё, чему нас учат про cache-friendly код правда.

И да, размер кэша растёт квадратично. Но если сравнить среднее время в полях по разному модулю, то прирост будет не сильно отличаться:

В реальном проекте, где был применён этот метод, матрицы не рандомные и прирост ещё заметнее.

Заключение

Можно ли ещё больше ускорить вычисление? К сожалению, больше ни одного способа я не знаю. Я думал над распараллеливанием вычислений, но обращение матриц очень плохо параллелизуется. Поэтому пока остаётся так.

Полный код я выложил на Pastebin.

Подробнее..

Какие шишки мы набили внедряя WhatsApp для бизнеса

13.12.2020 22:19:10 | Автор: admin

В этой статье я хочу рассказать от опыте внедрения мессенджера WhatsApp для осуществления коммуникаций с клиентами онлайн-платформы обучения детей английскому языку Novakid. Почему мы выбрали WhatsApp и с кем его внедрили, насколько это приложение помогает в ведении и развитии бизнеса, как помогает обслуживать клиентов и координировать работу сотрудников - подробности ниже.

Первые шаги

Использовать WhatsApp в Novakid начали в середине 2018 года, примерно через полгода после запуска стартапа. Количество учеников на платформе выросло с 0 до 1000 и появилась необходимость оперативной связи с клиентами. Практика показала, что это наиболее удобный и оперативный инструмент коммуникации. С помощью приложения сотрудники онлайн-школы не только напоминают о занятиях, но собирают обратную связь и комментарии о работе платформы.

Внедрение было тяжелым. WhatsApp только анонсировал запуск официального Business API, и мы одними из первых стали использовать это программное обеспечение для связи с клиентами. В какой-то момент мы стали даже рисковать использовать самописные решения, за которые могли получить постоянный бан от создателей мессенджера, рассказывают в Novakid.

Надо отметить, что WhatsApp не работает напрямую с конечным пользователем и не предоставляет доступ к своему API, этим занимается ограниченный список официальных партнеров (их список можно посмотреть здесь). Мы выбрали одну из доступных интеграций на маркетплейсе Intercom эта платформа используется у нас для работы с клиентами. Однако, и этот путь не привел к успеху присутствие сервиса-прослойки между прямым поставщиком WhatsApp API и Novakid значительно замедляло работу запросы обрабатывались очень медленно, шаблоны на отправку долго согласовывались, а с API было тяжело выстроить нормальную работу.

Цена общения

Череда ошибок на этапе запуска WhatsApp API для работы с клиентами привела нас к необходимости выбрать из списка официальных провайдеров бизнес-платформы мессенджера. После сравнения и анализа мы выбрали Gupshup.

Главными критериями выбора для нас были доступность API документации, наличие песочницы (тестового сервера) и стоимость. Если по первым двум пунктам предложения были плюс/минус похожи, то цены были очень разные

Расходы на официальное использование WhatsApp API для бизнеса складываются из нескольких элементов:

  • Комиссия по умолчанию, составляющая $0.048 за сообщения рассылки;

  • Плата за услуги провайдера (абонентская плата, плата за подключение, либо комиссия провайдера за каждое сообщение). При этом некоторые провайдеры берут все три комиссии сразу.

Мы провели анализ и поняли, что для нашей модели бизнеса выгоднее платить по факту и без всяких абонентских плат. Плата за каждое сообщение составляет около $0.049 и по $0.001 за сообщения в сессии.

Плюсы WhatsApp

WhatsApp запрещает отправлять рекламный спам пользователям. И, на наш взгляд, это плюс для обеих сторон: клиенту не нравится получать рекламу, а для бизнеса WhatsApp создает некий ограничивающий фактор, чтобы он не злоупотребил возможностями и не достал своего клиента. В WhatsApp мы отправляем сообщения пользователям при наступлении определенных событий. Например, закончился пробный урок, напишите обратную связь, назначили урок на 15:00 и т.п.

Отличие сообщений в WhatsApp от СМС-сообщений в количестве и качестве откликов. Со временем статистика показала, что клиенты почти никогда не отвечают на СМС-ки. В случае же с мессенджером, общение происходит сразу по факту уведомления. По логам сервера можно посмотреть живую историю переписки, необходимая информация оперативно доводится до конечного потребителя услуг.

Все это привело к более ответственному поведению родителей, которые забывали произвести оплату за занятие, например, из-за своей высокой занятости. Операторы Novakid ненавязчиво напоминают об окончании средств на счете и переводят их на страницу оплаты прямо в чате мессенджера.

И минусы

При работе с платформой провайдера WhatsApp есть свои недостатки, которые не могут быть видны клиентам. Так, например, не указывается причина отклонения заявок.

Однажды нам отклонили все шаблоны сообщений рассылки на турецком языке. Но, как стало известно, позднее, это формат работы WhatsApp INC., и провайдеры никак не могут на это повлиять, рассказывает сотрудница службы поддержки Novakid.

Еще одно небольшое неудобство связано с подготовкой и работой шаблонов. Сортировать их проблематично, все идет вперемешку с шаблонами по умолчанию, что создает дополнительные сложности, но они не критичны.

Подробнее..

Перевод Как я сократил время загрузки GTA Online на 70

01.03.2021 16:18:41 | Автор: admin
GTA Online. Многопользовательская игра, печально известная медленной загрузкой. Недавно я вернулся, чтобы завершить несколько ограблений и был потрясён, что она загружается настолько же медленно, как и в день своего выпуска, 7 лет назад.

Пришло время докопаться до сути.

Разведка


Сначала я хотел проверить, вдруг кто-то уже решил проблему. Но нашёл только рассказы о великой сложности игры, из-за чего она так долго загружается, истории о том, что сетевая p2p-архитектура мусор (хотя это не так), некоторые сложные способы загрузки в сюжетный режим, а потом в одиночную сессию, и ещё пару модов, чтобы скипнуть видео с логотипом R* во время загрузки. Ещё немного почитав форумы, я узнал, что можно сэкономить колоссальные 10-30 секунд, если использовать все эти способы вместе!

Тем временем на моём компе

Бенчмарк


Story mode load time:  ~1m 10sOnline mode load time: ~6m flatStartup menu disabled, time from R* logo until in-game (social club login time isn't counted).Old but decent CPU:   AMD FX-8350Cheap-o SSD:          KINGSTON SA400S37120GWe have to have RAM:  2x Kingston 8192 MB (DDR3-1337) 99U5471Good-ish GPU:         NVIDIA GeForce GTX 1070

Знаю, что моё железо устарело, но чёрт возьми, что может замедлить загрузку в 6 раз в онлайн-режиме? Я не мог измерить разницу при загрузке из сюжетного режима в онлайн, как это делали другие. Даже если это сработает, разница небольшая.

Я (не) одинок


Если доверять этому опросу, проблема достаточно широко распространена, чтобы слегка раздражать более 80% игроков. Прошло уже семь лет!



Я немного поискал информацию о тех ~20% счастливчиках, которые загружаются быстрее трёх минут, и нашёл несколько бенчмарков с топовыми игровыми ПК и временем загрузки онлайн-режима около двух минут. Я бы кого-нибудь убил хакнул за такой комп! Действительно похоже на железячную проблему, но что-то не складывается

Почему у них сюжетный режим по-прежнему загружается около минуты? (кстати, при загрузке с M.2 NVMe не учитывались видео с логотипами). Кроме того, загрузка из сюжетного режима в онлайн занимает у них всего минуту, в то время как у меня около пяти. Я знаю, что их железо гораздо лучше, но не в пять же раз.

Высокоточные измерения


Вооружившись таким мощным инструментом, как Диспетчер задач, я приступил к поиску узкого места.



Почти минута уходит на загрузку общих ресурсов, которые нужны и для сюжетного режима, и для онлайна (почти наравне с топовыми ПК), затем GTA в течение четырёх минут полностью нагружает одно ядро CPU, больше ничего не делая.

Использование диска? Нет! Использование сети? Есть немного, но через несколько секунд падает в основном до нуля (кроме загрузки вращающихся информационных баннеров). Использование GPU? Ноль. Память? Вообще ничего

Что это, майнинг биткоинов или что-то такое? Чую здесь код. Очень плохой код.

Единственный поток


На моём старом процессоре AMD восемь ядер, и он ещё молодцом, но это старая модель. Его сделали ещё тогда, когда производительность одного потока у AMD была намного ниже, чем у Intel. Наверное, это главная причина таких различий во времени загрузки.

Что странно, так это способ использования CPU. Я ожидал огромное количество операций чтения с диска или массу сетевых запросов, чтобы организовать сеансы в сети p2p. Но такое? Вероятно, здесь какая-то ошибка.

Профилирование


Профилировщик отличный способ найти узкие места в CPU. Есть только одна проблема большинство из них полагаются на инструментирование исходного кода, чтобы получить идеальную картину происходящего в процессе. А у меня нет исходного кода. Мне также не требуются идеальные показания в микросекундах, у меня узкое место на 4 минуты.

Итак, добро пожаловать в образцы стека (stack sampling). Для приложений с закрытым исходным кодом есть только такой вариант. Сбросьте стек запущенного процесса и местоположение указателя текущей инструкции, чтобы построить дерево вызовов в заданные интервалы. Затем наложите их и получите статистику о том, что происходит. Я знаю только один профилировщик, который может проделать это под Windows. И он не обновлялся уже более десяти лет. Это Люк Stackwalker! Кто-нибудь, пожалуйста, подарите Люку немножко любви :)



Обычно Люк группировал бы одинаковые функции, но у меня нет отладочных символов, поэтому пришлось смотреть на соседние адреса, чтобы искать общие места. И что же мы видим? Не одно, а целых два узких места!

Вниз по кроличьей норе


Позаимствовав у моего друга совершенно законную копию стандартного дизассемблера (нет, я действительно не могу его себе позволить когда-нибудь освою гидру), я пошёл разбирать GTA.



Выглядит совсем неправильно. Да, у большинства топовых игр есть встроенная защита от реверс-инжиниринга, чтобы защититься от пиратов, мошенников и моддеров. Не то чтобы это их когда-то останавливало

Похоже, здесь применили какую-то обфускацию/шифрование, заменив большинство инструкций тарабарщиной. Не волнуйтесь, нужно просто сбросить память игры, пока она выполняет ту часть, на которую мы хотим посмотреть. Инструкции должны быть деобфусцированы перед запуском тем или иным способом. У меня рядом лежал Process Dump, так что я взял его, но есть много других инструментов для подобных задач.

Проблема первая: это strlen?!


Дальнейший разбор дампа выявил один из адресов с некоей меткой strlen, которая откуда-то берётся! Спускаясь вниз по стеку вызовов, предыдущий адрес помечен как vscan_fn, и после этого метки заканчиваются, хотя я вполне уверен, что это sscanf.

Куда ж без графика

Он что-то парсит. Но что? Логический разбор займёт целую вечность, поэтому я решил сбросить некоторые образцы из запущенного процесса с помощью x64dbg. Через несколько шагов отладки выясняется, что это JSON! Он парсит JSON. Колоссальные десять мегабайт JSON'а с записями 63 тыс. предметов.

...,{    "key": "WP_WCT_TINT_21_t2_v9_n2",    "price": 45000,    "statName": "CHAR_KIT_FM_PURCHASE20",    "storageType": "BITFIELD",    "bitShift": 7,    "bitSize": 1,    "category": ["CATEGORY_WEAPON_MOD"]},...

Что это? Судя по некоторым ссылкам, это данные для сетевого торгового каталога. Предполагаю, он содержит список всех возможных предметов и обновлений, которые вы можете купить в GTA Online.

Проясним некоторую путаницу: я полагаю, что это предметы, приобретаемые за игровые деньги, не связанные напрямую с микротранзакциями.

10 мегабайт? В принципе, не так уж и много. Хотя sscanf используется не самым оптимальным образом, но, конечно, это не так уж плохо? Что ж



Да, такая процедура займёт некоторое время Честно говоря, я понятия не имел, что большинство реализаций sscanf вызывают strlen, поэтому не могу винить разработчика, который написал это. Я бы предположил, что он просто сканировал байт за байтом и мог остановиться на NULL.

Проблема вторая: давайте использовать хэш-массив?


Оказывается, второго преступника вызывают сразу за первым. Даже в одной и той же конструкции if, как видно из этой уродливой декомпиляции:



Все метки мои, и я понятия не имею, как на самом деле называются функции/параметры.

Вторая проблема? Сразу после разбора элемента он хранится в массиве (или встроенном списке C++? не уверен). Каждая запись выглядит примерно так:

struct {    uint64_t *hash;    item_t   *item;} entry;

А перед сохранением? Он проверяет весь массив, сравнивая хэш каждого элемента, есть он в списке или нет. С 63 тыс. записей это примерно (n^2+n)/2 = (63000^2+63000)/2 = 1984531500, если я не ошибаюсь в расчётах. И это в основном бесполезные проверки. У вас есть уникальные хэши, почему не использовать хэш-карту.



Во время реверс-инжиниринга я назвал его hashmap, но это явно не_hashmap. И дальше ещё интереснее. Этот хэш-массив-список пуст перед загрузкой JSON. И все элементы в JSON уникальны! Им даже не нужно проверять, есть они в списке или нет! У них даже есть функция прямой вставки элементов! Просто используйте её! Серьёзно, ну ребята, что за фигня!?

PoC


Всё это конечно классно, но никто не воспримет меня всерьёз, пока я не напишу реальный код для ускорения загрузки, чтобы сделать кликбейтный заголовок для поста.

План такой. 1. Написать .dll, 2. внедрить её в GTA, 3. зацепить некоторые функции, 4. ???, 5. профит. Всё предельно просто.

Проблема с JSON нетривиальная, я не могу реально заменить их парсер. Более реалистичным кажется заменить sscanf на тот, который не зависит от strlen. Но есть ещё более простой способ.

  • зацепить strlen
  • подождать длинной строки
  • закэшировать начало и длину
  • если поступит ещё вызов в пределах диапазона строки, вернуть закэшированное значение

Что-то вроде такого:

size_t strlen_cacher(char* str){  static char* start;  static char* end;  size_t len;  const size_t cap = 20000;  // if we have a "cached" string and current pointer is within it  if (start && str >= start && str <= end) {    // calculate the new strlen    len = end - str;    // if we're near the end, unload self    // we don't want to mess something else up    if (len < cap / 2)      MH_DisableHook((LPVOID)strlen_addr);    // super-fast return!    return len;  }  // count the actual length  // we need at least one measurement of the large JSON  // or normal strlen for other strings  len = builtin_strlen(str);  // if it was the really long string  // save it's start and end addresses  if (len > cap) {    start = str;    end = str + len;  }  // slow, boring return  return len;}


А что касается проблемы хэш-массива, то здесь просто полностью пропускаем все проверки и вставляем элементы напрямую, поскольку мы знаем, что значения уникальны.

char __fastcall netcat_insert_dedupe_hooked(uint64_t catalog, uint64_t* key, uint64_t* item){  // didn't bother reversing the structure  uint64_t not_a_hashmap = catalog + 88;  // no idea what this does, but repeat what the original did  if (!(*(uint8_t(__fastcall**)(uint64_t*))(*item + 48))(item))    return 0;  // insert directly  netcat_insert_direct(not_a_hashmap, key, &item);  // remove hooks when the last item's hash is hit  // and unload the .dll, we are done here :)  if (*key == 0x7FFFD6BE) {    MH_DisableHook((LPVOID)netcat_insert_dedupe_addr);    unload();  }  return 1;}

Полный исходный код PoC здесь.

Результаты


Ну и как оно работает?

Original online mode load time:        ~6m flatTime with only duplication check patch: 4m 30sTime with only JSON parser patch:       2m 50sTime with both issues patched:          1m 50s(6*60 - (1*60+50)) / (6*60) = 69.4% load time improvement (nice!)

Да, чёрт возьми, получилось! :))

Скорее всего, это не решит всех проблем с загрузкой в разных системах могут быть и другие узкие места, но это такая зияющая дыра, что я понятия не имею, как R* пропустила её за все эти годы.

Краткое содержание


  • При запуске GTA Online есть узкое место, связанное с однопоточным вычислением
  • Оказалось, GTA изо всех сил пытается распарсить 1-мегабайтный файл JSON
  • Сам парсер JSON плохо сделан/наивен и
  • После парсинга происходит медленная процедура удаления дублей

R*, пожалуйста, исправьте


Если информация каким-то образом дойдёт до инженеров Rockstar, то проблему можно решить в течение нескольких часов силами одного разработчика. Пожалуйста, ребята, сделайте что-нибудь с этим :<

Вы можете либо перейти на хэш-карту для удаления дублей, либо полностью пропустить дедупликацию при запуске как быстрое исправление. Для парсера JSON просто замените библиотеку на более производительную. Не думаю, что есть более простой вариант.

ty <3
Подробнее..

Как я автоматизировал однотипную, монотонную работу бесплатно и без навыков программирования

27.04.2021 00:04:49 | Автор: admin

Многим знакома проблема однотипных действий за компьютером, особенно на офисных работах. Заполнение каких-то данных в таблицу, размещение постов в социальных сетях, поиск в базе данных и заполнение отчетов. И если для программистов решение такой задачи не составит труда, то что делать обычным офисным работникам?

Я долго мучался над подобной проблемой, был период когда вся моя работа заключалась в сравнении данных в Excel, SAP и ещё парочки источников, но так как я очень ленив, стал искать способ как это дело можно автоматизировать и вот уже два года, я с некой периодичностью создаю новые или редактирую старые программы действий для компьютера (скрипты или сценарии), позволяя ему работать вместо меня, а себе гонять чай в рабочее время посматривая на экран. Я не стал супер экспертом по автоматизированию (RPA разработка если по научному), но работа стала приносить намного меньше стресса.

И если про автоматизацию работы в Excel уже написано немало постов даже для чайников, то что делать тем, кто работает вне этой замечательной программы? Для этого я хотел бы рассказать про программы настраиваемых автокликеров, на примереclikermann.

Почему она?
- Она бесплатная. Совсем. Никаких пробных версий, никаких реклам в самой программе, создана на чистом энтузиазме и добровольных пожертвованиях автору.
- Интерфейс максимально прост, как и язык написания сценариев.
- Есть возможность записи действий, можно полностью обойтись без редактирования или создания с нуля своего скрипта.
- Встроенный редактор.
- Не требуются права администратора для установки, скачал - запустил.
- Русскоязычная справка по программе и русскоязычное сообщество.

Что можно с помощью неё делать?
Лично я автоматизировал работу с 1С, SAP, сайтами, тем же Excel, публикации в социальных сетях. Так же можно запрограммировать действия в играх, реакцию на происходящее на экране, набор текста, буквально что угодно.

познакомимся с интерфейсом программы:

Он минималистичен и тем хорош. Создадим свой первый скрипт и попробуем написать какой-нибудь простой скрипт, своего рода Hello world от мира автокликинга.

Нажимаем на кнопку создания скрипта (1) и кнопку редактора (3).

Интерфейс редактора


Нажимаем на сохранить как (4) выбираем место, где он будет храниться (желательно оставить его в папке с программой) и имя сценария.

Например, создадим скрипт который будет брать адрес страницы из таблицы Excel, открывать его в браузере и так несколько раз. Вот что у нас получилось:

https://cs14.pikabu.ru/video/2021/04/11/1618089701287548721_1920x1080.webm

Мы используем несколько команд:

WAIT- небольшая задержка между выполнениями действий, нужна для того, чтобы компьютер успевал открывать страницы, переключать между окнами и так далее, нужна почти между всеми действиями.

LCLICK- Кликает левой кнопкой мыши по указанным в скобках координатам, необходимости прописывать их в ручную нет, просто наводите мышь туда, куда хотите кликнуть и нажимаете комбинациюALT + Q. Команда с координатами вставляется в скрипт автоматически. Либо если хотите вручную их поправить, опять же наводим мышь на желаемое расположение и смотрим сюда:

RCLICK- Соответственно всё тоже самое, только правой кнопкой мыши, вызывается тем же набором клавиш, просто нужно заменить в скрипте L на R.

Шаблоныкопирования и вставки (CTRL + C, CTRL +V) они уже есть в разделе шаблоны, нет необходимости их прописывать самому, просто выбираем подходящий.

KEYPRESS- В том случае, если нам нужно нажать какую либо кнопку, выбираем эту команду, кстати чтобы не прописывать в ручную, эту команду тоже можно выбрать из списка команд:

Возможно на первый раз покажется, что это довольно сложно, однако поработав в программе несколько минут вы уже привыкните к такому пошаговому описанию действий и легко будете разбираться в них.

Подробнее..

Pythonnet. Как запустить C код из Python

10.05.2021 10:05:19 | Автор: admin

Введение

На сегодняшний день Python является одним из самых популярных языков программирования, но даже это не помогает ему покрыть все потребности программистов. Самый очевидный минус чистого CPython - это его скорость, поэтому некоторые программисты выбирают для своих задач другие языки программирования, а кто-то просто реализует узкие места на C/C++ и подключает их к Python.

Однако бывают случаи, когда есть некая база кода, написанного на C#, а возможности быстро переписать всё на Python/C/C++ нет. Тогда встает вопрос как подключить C# к Python?. Для этого была разработана библиотека pythonnet. В этой статье разберем: как запустить C# код из Python и что из этого может получиться.

Реализация

Для сравнения скорости выполнения C# и Python я буду ссылаться на одну из прошлых статей.

Библиотека pythonnet работает с .dll файлами, поэтому весь код необходимо будет преобразовывать в динамически подключаемые библиотеки. Чтобы создать .dll файл из C# необходимо установить visual studio и при создании проекта указать, что проект будет создан для библиотеки классов (я дал название проекту: MyTestCS, в будущем dll файл будет носить такое же название как и проект):

В качестве примера будем использовать магазин из прошлой статьи, который оптимизировали силами самого питона и других языков. Создадим структуру для одного товара на C#:

public struct DataGoods    {        public string name;        public int price;        public string unit;        public DataGoods(string name, int price, string unit)        {            this.name = name;            this.price = price;            this.unit = unit;        }    }

Теперь реализуем класс самого магазина. В нем создадим метод для заполнения магазина товарами:

public class ShopClass    {        public string name;        public List<DataGoods> listGoods;        public ShopClass(string name)        {            this.name = name;this.listGoods = new List<DataGoods>();        }        /// <summary>        /// Метод для создания товаров в магазине        /// </summary>        /// <param name="numberGoods"> Количество объектов в магазине </param>        public void createShopClass(int numberGoods) {            List<DataGoods> lGoods = new List<DataGoods>();            for (int i = 0; i < numberGoods; i++) {                lGoods.Add(new DataGoods("телефон", 20000, "RUB"));                lGoods.Add(new DataGoods("телевизор", 45000, "RUB"));                lGoods.Add(new DataGoods("тостер", 2000, "RUB"));            }            this.listGoods = lGoods;        }   }

После того, как класс был создан, приступим к подключению C# кода к Python проекту. Сначала создадим .dll файл из C# проекта (достаточно нажать команду ctrl+shift+B). В папке bin->debug->netstandart2.0 проекта (путь зависит от того, какие конфигурации среды стоят у вас) появится файл с названием проекта и расширением .dll (именно этот файл будет подключаться к программе на Python).

Далее разберемся с проектом на Python. Необходимо установить библиотеку pythonnet, выполнив команду:

pip install pythonnet

В проекте создадим файл main.py, а также поместим библиотеку MyTestCS.dll в папку с проектом:

Теперь можно подключать библиотеку в main.py, для этого сначала импортируем clr (clr позволяет рассматривать пространства имен CLR как пакеты Python):

import clr

Укажем путь до нашего .dll файла:

pathDLL = os.getcwd() + "\\MyTestCS.dll"

Чтобы подгрузить нужную нам библиотеку необходимо прописать следующий код:

clr.AddReference(pathDLL)

После чего можно импортировать модуль и всё, что в нем содержится. Если напрямую сделать импорт MyTestCS:

import MyTestCSprint(MyTestCS)>>> <module 'MyTestCS'>

То можно увидеть, что наш проект загрузился как модуль. Также можно напрямую импортировать необходимые данные, например, необходимые классы или структуры из проекта.

Создадим экземпляр класса ShopClass и DataGoods через Python и обратимся к полям этих классов.

from MyTestCS import ShopClass, DataGoodsshop = ShopClass("Тест магазин")shop.createShopClass(1)goods = DataGoods("чехол для телефона", 500, "RUB")print(shop.name)>>> Тест магазинprint(shop.listGoods)>>> [<MyTestCS.DataGoods object at 0x000001D04C3FE3C8>, <MyTestCS.DataGoods object at 0x000001D04C3FE438>, <MyTestCS.DataGoods object at 0x000001D04C3FE400>]print(shop.listGoods[1].name, shop.listGoods[1].price, shop.listGoods[1].unit)>>> телевизор 45000 RUBprint(goods.name, goods.price, goods.unit)>>> чехол для телефона 500 RUB

Как итог, получилось вызвать код C# из Python и поработать с классами. Теперь протестируем производительность создания 200*100000 товаров через метод createShopClass:

shop = ShopClass("Тест магазин")s = time.time()shop.createShopClass(200 * 100000)print("СОЗДАНИЕ ТОВАРОВ НА C#:", time.time() - s)>>> СОЗДАНИЕ ТОВАРОВ НА C#: 2.9043374061584473

В прошлой статье время создания такого количества товаров заняло примерно 44 секунды. Использование C# вместо Python позволило ускорить этот процесс примерно в 15 раз, что является очень хорошим результатом.

Проблемы

Однако не может же быть всё настолько хорошо, чтобы броситься переписывать куски кода Python на C#. И это так. Попробуем из Python вручную дополнить товарами магазин:

shop = ShopClass("Тест магазин 1")s = time.time()shop.createShopClass(500000)print("СОЗДАЛИ ТОВАР ЧЕРЕЗ C#:", time.time()-s)>>> СОЗДАЛИ ТОВАР ЧЕРЕЗ C#: 0.07325911521911621shop = ShopClass("Тест магазин 2")s = time.time()for _ in range(500000):        goods1 = DataGoods("телефон", 20000, "RUB")        goods2 = DataGoods("телевизор", 45000, "RUB")        goods3 = DataGoods("тостер", 2000, "RUB")        shop.listGoods.extend([goods1, goods2, goods3])print("СОЗДАЛИ ТОВАР ЧЕРЕЗ PYTHON:", time.time()-s)>>> СОЗДАЛИ ТОВАР ЧЕРЕЗ PYTHON: 5.2899720668792725

И проверим аналогичный код, написанный на Python:

istGoods = []class DataGoods2:        def __init__(self, name, price, unit):            self.name = name            self.price = price            self.unit = units = time.time()for _ in range(500000):        goods1 = DataGoods2("телефон", 20000, "RUB")        goods2 = DataGoods2("телевизор", 45000, "RUB")        goods3 = DataGoods2("тостер", 2000, "RUB")        listGoods.extend([goods1, goods2, goods3])print("СОЗДАЛИ PYTHON ОБЪЕКТ:", time.time()-s)>>> СОЗДАЛИ PYTHON ОБЪЕКТ: 1.2972710132598877

Код чистого питона работает быстрее, чем дополнение объекта, созданного из модуля C#. Это связано с тем, что доступ к объектам, написанным на C#, занимает довольно много времени. Чтобы избежать таких проблем, необходимо писать всю логику работы с классом внутри C# кода, и не выносить эту логику в Python. Изменение скорости выполнения кода будет заметно при подсчете суммы всех товаров. Реализуем функцию подсчета суммы товаров на C# (внутри класса ShopClass):

public long getSumGoods() {    long sumGoods = 0;    foreach (DataGoods goods in this.listGoods) {      sumGoods += goods.price;    }    return sumGoods;}

А также на Python:

shop = ShopClass("Магазин 3")shop.createShopClass(1000000)s = time.time()shop.getSumGoods()print("ВРЕМЯ НА СУММУ ТОВАРОВ C#:", time.time()-s)>>> ВРЕМЯ НА СУММУ ТОВАРОВ C#: 0.0419771671295166sumGoods = 0for goods in shop.listGoods:     sumGoods += goods.priceprint("ВРЕМЯ НА СУММУ ТОВАРОВ PYTHON:", time.time()-s)>>> ВРЕМЯ НА СУММУ ТОВАРОВ PYTHON: 6.205681085586548

Python код выполняется гораздо медленнее, чем внутренние методы C#.

Многопоточность

Так как в C# отсутствует GIL, то мне стало интересно протестировать работу многопоточности в C# и попробовать запустить потоки в C# через Python. Для начала протестируем протестируем создание 3х классов ShopClass последовательно и заполним их 3.000.000 товаров:

public class testShop    {        public void testSpeedNoThread(int count)        {            testShopClass(count);            testShopClass(count);            testShopClass(count);        }        public static void testShopClass(int count)        {            ShopClass shop = new ShopClass("Магазин");            shop.createShopClass(count);        }}

Python код для запуска:

tshop = testShop()s = time.time()tshop.testSpeedNoThread(3000000)print("СОЗДАЕМ ПОСЛЕДОВАТЕЛЬНО 3 МАГАЗИНА:", time.time()-s)>>> СОЗДАЕМ ПОСЛЕДОВАТЕЛЬНО 3 МАГАЗИНА: 2.1849117279052734

Дополним класс testShop для работы с потоками новым методом:

public static void testThread(){    ExThread obj = new ExThread();    Thread thr = new Thread(new ThreadStart(obj.mythread1));    Thread thr2 = new Thread(new ThreadStart(obj.mythread1));    Thread thr3 = new Thread(new ThreadStart(obj.mythread1));    thr.Start();    thr2.Start();    thr3.Start();    thr.Join();    thr2.Join();    thr3.Join();}

И создадим новый вспомогательный класс:

public class ExThread{   public void mythread1()     {         ShopClass shop = new ShopClass("Магазин");         shop.createShopClass(3000000);     }}

Запустим Python код для проверки работы потоков:

s = time.time()tshopThread = testShop()tshopThread.testThread()print("СОЗДАЕМ 3 ПОТОКА C# ДЛЯ 3х МАГАЗИНОВ:", time.time()-s)>>> СОЗДАЕМ 3 ПОТОКА C# ДЛЯ 3х МАГАЗИНОВ: 0.6765928268432617

Вывод

Использование частей кода, написанных на C# в Python возможно, но при таком подходе есть и свои минусы, например, скорость доступа к объектам. Использование pythonnet целесообразно, если имеются какие-то части кода, которые нет возможности переписать на Python, но они требуют подключения к основному проекту на Python.

P.S. есть и другие способы ускорить python, например, написать библиотеку на C/C++ или переписать часть кода на Cython с меньшими проблемами. В данной статье лишь представлена возможность использования C# и Python вместе. Также существует реализация Python для платформы Microsoft.NET под названием IronPython.

Подробнее..

Жадный алгоритм, ветви и границы для расписания мерчендайзеров (кейс Хакатона на оптимизацию)

11.05.2021 14:17:08 | Автор: admin

Это пилотная статья. Будем благодарны за обратную связь. Если тема вызовет интерес, мы возможно примем решение выложить на GitHub наши исходники (python) и входные data-setы.

В марте 2021 г. случилось мне поучаствовать в хакатоне с задачей на комбинаторику и оптимизацию. Команду решил собрать свежую, из одиночек, дрейфующих в пуле самого хака. Довольно быстро нашлись front и back, и втроем мы принялись старательно думать, как потратим деньги, когда выиграемJ Так как сам кейс показался нам по началу не сложным. Надо сказать, что в хаках я не так давно, но уже успел поучаствовать и в ЛЦТ(Лидеры Цифровой Трансформации), и в Цифровом Прорыве. В последнем даже удалось занять бронзу в финале. Роль всегда у меня была project+product+ppt (хотя опыт в программировании у меня также имеется). Не редко в хакатонских кейсах проблемы немного надуманы, решения этих проблем немного фееричны и не несут практического смысла, а побеждает профессиональная преза и поставленный питч. Так вот этот мартовский хакатон меня заинтересовал живостью и насущностью бизнес проблем, которые там решались. Опытные хакатонщики, читающие эти строки, поймут. Но полно про хакатоны и про то, какие они бывают, а то собьемся с курса.

В этам хаке преза и даже front мало интересовали кейсодержателей. Это был натуральный бизнес хакатон с абсолютно живыми дата сетами и с натуральной бизнес болью. Кстати на хакатоне ни одна из команд не предоставила решение. Вроде одни ребята "нарисовали" в своей презе оптимизацию в минус 1 агент, но демонстрацию на защите они не представили. Наша команда не стала исключением и заняла скромное третье место (всего до защиты дошло 5 команд). Теперь наконец к условию:

Есть 204 ТТ (торговые точки). Каждую из них нужно посетить минимум раз в неделю, а некоторые больше одного раза. Длительности посещения каждой ТТ известны и не изменяются внутри недели.

Также предоставлена матрица 204х204 с расстояниями между точками (см. Табл.1 ниже ). Матрица не симметричная, т.к. при составлении учитывались знаки ПДД (одностороннее движение и пр.). Видимо она была получена через API Яндекс.Карт по адресам ТТ. Требовалось найти оптимальное расписание для торговых агентов (мерчендайзеров), улучшив показатели текущего AS IS (оно тоже предоставлено). Важные ограничения: каждая ТТ должна быть закреплена за определенным мерчендайзером (торговым агентом). Другими словами, если торговый агент (далее агент) посещает Магнит на улице Ленина в понедельник, то этот же Магнит в другие дни недели должен посещать именно он. Время работы каждого агента не должно превышать 9,5 часов в день с учетом работы в ТТ и перемещению между ними.

0

1

2

...

203

0

0

4031

4152

....

8853

1

4021

0

817

....

10196

2

4239

926

0

....

10306

....

....

....

....

0

10345

203

10071

10610

10289

10886

0

Таблица 1. Матрица расстояний между точками

Предоставленное расписании AS IS распределено на 14 агентов. И как вы уже догадались, решение должно было предложить альтернативное расписание, сократив по возможности количество агентов и их время в пути.

Спустя какое-то время после хакатона наш back(python) призвал на помощь коллегу с курсов по deep learning. Парни погуглили, поискали готовый алгоритм, почитали про метод отжига, муравьев, генетику и пришли еще раз к выводу, что подходящего метода и примеров кода именно для этой задачи, куда можно было бы взять и вставить матрицу расстояний, как входной параметр на просторах интернета - нет. Если кто знает где есть поделитесь, будем благодарны.

Позднее, и даже в параллель я сам начал изучать тему Задачи коммивояжера и эвристические методы, зарекомендовавшие себя для ее решения. Но ни хабр, ни youtube, ни Википедия не давали готового ответа именно для этого частного случая. Останавливаться все равно не хотелось. Наоборот, по мере продвижения вперед интерес подогревался. Мотивации добавило то, что классическая задача коммивояжера относится к NP полным задачам. Если решать задачу полным перебором (brutal force), то уже при 66 торговых точках нужно несколько миллиардов лет и компьютер размером с Землю. Согласитесь, мощь трансвычислительных задач завораживает! И действительно, для решения классической задачи TSP (Travelling salesman problem ) существуют различные эвристические методы, такие как Метод Имитации Отжига, Муравьиный алгоритм, Генетический метод и др. Оставалось решить один вопрос: как применить что-то из этой эвристики к сквозному недельному расписанию нескольких торговых агентов с ограничением по времени. Возможно толковый математик, который кожей чувствует физический смысл дифференцирования, логарифмов, пределов, силу числа e и т.д., справился бы с этой задачей. Но я не такой математик. Формулы счастья по-прежнему в интернете не находилось. Я даже изучил основы нейросетей, но когда я начинал проектировать свою нейросеть для решения - вырастала непреодолимая стена.

И все-таки решение родилось! Ну во-первых я обратился за помощью к своему коллеге, Черкасову Евгению @eny01. Он как раз недавно прошел курсы по data science и python, и рвался в бой, чтобы применить все приобретённые навыки. К слову сказать, бОльшую часть алгоритма именно он и разработал. Себе я взял часть, отвечающую за рекурсивный метод ветвей и границ. Но об этом позже. Вдвоем мы разработали следующий план.

  1. Мы декомпозировали задачу до каждого агента. Набираем ТТ сначала для одного агента до предела по очереди для всех дней недели, затем для второго и т.д.;

  2. Процесс набора ТТ происходит с помощью жадного алгоритма (метод ближайшего соседа);

  3. В случае, когда набор подходит к пределу (9,5 часов) - применяем оптимизацию и считаем, что для данного агента в этот день недели оптимальное расписание составлено. Переходим к следующему дню;

Алгоритм набора расписания для каждого агента более подробно можно увидеть на блок-схеме ниже:

Теперь чуть подробнее про основные моменты:

  1. Ближайшие сосед:

Первой точкой расписания дня недели мы берем точку, наиболее объемную по времени посещения и наиболее частую в неделе. Второй и последующими точками становятся ближайшие к уже отобранным точкам. Каждый раз при добавлении новой точки в расписание дня производится оптимизация по подбору наилучшей первой точки для получаемого кратчайшего пути в расписании. Мы попросту перебираем точки, подставляя каждую в качестве первой и перенабираем к ней при помощи того же жадного алгоритма отдельный массив. В итоге выбираем самый лучший вариант по минимальному пути для прохождения точек.

Важно следить за переполнением не только в том дне, в котором идет набор, но и параллельно приглядывать за остальными днями торгового представителя- последующими и предыдущими. Так как выбирая точку, скажем для четверга, мы можем переполнить ею пятницу или понедельник.

Когда в одном из дней достигается предел рабочего времени (>=9,5 ч.), система сбрасывает точку приведшую к переполнению и пробует добрать следующую ближайшую. Кстати, мы пробовали вставлять сюда вместо оптимизации "первой точки" и ближайшего соседа полный перебор с ветвями и границами. И результат, !внимание!, получался в итоге тот же. Но времени на перебор с ветвями и границами потребовалось гораздо больше. Около 3,5 часов против 5 минут для всех 204 точек. Таким образом полный перебор был абсолютно избыточен и неэффективен при частом применении. Вследствие чего в этом месте было решено от перебора с ветвями и границами отказаться, но добавить его в финальную оптимизацию составленного расписания.

2. Метод ветвей и границ:

Ну тут все относительно просто. Утвержденное недельное расписание мы прогоняем через полный перебор, используя метод ветвей и границ. Реализация этой функции потребовала применения рекурсии. Причем если сравнивать работу полного перебора через библиотеку itertools (python), то время на перебор 8 точек составило 21 сек, в то время как наша рекурсия с ветвями и границами отрабатывала за 0.4 с. Что не может не радовать! Цель применения ветвей и границ - оптимизация уже готовых расписаний. Привязка ТТ и агентов уже не меняется, но оптимизируется время в пути (решаем классическую задачу коммивояжера).

Результаты:

Описанным выше образом мы производим набор точек для второго агента, затем для третьего и т.д. В результате работы алгоритма нам удалось не только снизить количество агентов с 14 до 13, но и сократить суммарную траекторию перемещения между ТТ вдвое. Ниже представлены результаты, которых нам удалось добиться:

Параметр

До оптимизации

После оптимизации (с брутфорсом в наборе расписания)

Дельта

Относительное изменение, %

Число Мерчендайзеров

14

13

1

7,14%

Суммарная средняя траектория всех ТП, км

25,06

13,54

11,52

45,96%

Суммарная траектория всех маршрутов всех ТП, км

1729,342

839,69

889,65

51,44%

Средняя длительность рабочего дня для ТП

469,43

508,03

-38,6

-8,22%

Общая продолжительность работы всех ТП, час

539,85

524,97

14,88

2,76%

Время работы алгоритма для всех агентов на всех днях недели составило 8 минут на домашнем ПК.

И в качестве финального аккорда мы позволили себе выкрутить лимит с 9 часов 30 минут до 9 часов 38 минут. И получили сокращение до 12 агентов с небольшой погрешностью. Из 60 дневных расписаний 14 уходят в переработки от 1 до 8 минут (51 минута в сумме).

Ждем ваших комментариев, замечаний и предложений! Ответим на все вопросы. Пишите, как бы вы решали эту задачу. Особенно ценны для нас будут мнения практиков. Как математиков, так и data science'ов. Возможно кто-то предложит существующие библиотеки python для решения задачи. Мы, как я уже сказал, подходящих библиотек не нашли. Всем спасибо, что дочитали до конца!

Подробнее..

Перевод Максимально оптимизированная веб-загрузка изображений в 2021 году

20.02.2021 16:15:19 | Автор: admin

В этой статье я расскажу про 8 методик оптимизации загрузки изображений, которые уменьшают необходимую пропускную способность сети и нагрузку на процессор при выводе на экран. Приведу примеры аннотированного HTML, чтобы вам было легче воспроизвести. Какие-то методики уже давно известны, а какие-то появились относительно недавно. В идеале, ваш любимый механизм публикации веб-документов (например, CMS, генератор статичных сайтов или фреймворк для веб-приложений) должен всё это реализовывать из коробки.

В совокупности методики оптимизируют все элементы Google Core Web Vitals с помощью:

  • минимизации основных проблем содержимого (Largest Contentful Paint (LCP)) за счёт уменьшения размеров, кеширования и ленивой загрузки;
  • сохранения нулевого накопительного сдвига макета (Cumulative Layout Shift (CLS));
  • уменьшения задержки первого ввода (First Input Delay (FID)) за счёт снижения потребления процессора (для основного потока исполнения).

Чтобы увидеть в действии работу всех методик, посмотрите на исходный код загрузки этого изображения:

https://www.industrialempathy.com/img/remote/ZiClJf.jpg<img loading="lazy" decoding="async" style="background-size: cover; background-image: none;" src="http://personeltest.ru/aways/habr.com/img/remote/ZiClJf.avif" alt="Sample image illustrating the techniques outlined in this post." width="4032" height="2268">


Методики оптимизации


Отзывчивый макет


Это понятная методика позволяет изображению занимать доступное горизонтальное пространство с сохранением соотношения сторон. В 2020-м браузеры научились резервировать корректное количество вертикального пространства под изображение до его загрузки, если в элементе img указаны атрибуты width и height. Это позволяет избежать накопительного сдвига макета.

<style> img {   max-width: 100%;   height: auto; }</style><!-- Providing width and height is more important than ever. --><img height="853" width="1280"  />

Ленивая отрисовка


Вторая методика сложнее. Новый CSS-атрибут content-visibility: auto говорит браузеру не думать о размещении изображения, пока оно не будет готово. У этого подхода несколько достоинств, главное из которых в том, что пока браузер не получит размытое изображение-заглушку или само изображение, он не будет его декодировать, экономя ресурсы процессора.

Больше не нужен contain-intrinsic-size


В более ранней версии статьи объяснялось, как с помощью contain-intrinsic-size избежать эффекта CLS при использовании content-visibility: auto. Но в Chromium 88 это больше не нужно в случае с изображениями, для которых указаны width и height. По состоянию на 27 января 2021 года content-visibility: auto ещё не реализован в других браузерных движках, вероятно, они последуют примеру Chromium. Так что да, теперь с этим гораздо проще!

<style> /* This probably only makes sense for images within the main scrollable area of your page. */ main img {   /* Only render when in viewport */   content-visibility: auto; }</style>

AVIF


AVIF самый свежий графический формат, который получил поддержку в браузерах. Сейчас он поддерживается в Chromium и по флагу в Firefox. Safari с ним пока не работает, но поскольку Apple является участником группы, разработавшей формат, в будущем этот браузер тоже должен поддерживать AVIF.

Этот формат примечателен тем, что он значительно превосходит JPEG. И этим выгодно отличается от формата WebP, изображения в котором не всегда меньше JPEG и который может повышать потребление ресурсов из-за отсутствия поддержки прогрессирующей загрузки.

Для реализации прогрессирующего расширения для AVIF можно воспользоваться элементом picture.

Фактически элемент img вложен в picture. Это может запутать, потому что img иногда называют запасным решением для браузеров, не поддерживающих picture, но по сути этот элемент помогает лишь с выбором src, а своего макета у него нет. Отрисуется именно элемент img, к нему вы и будете применять стиль.

До недавнего времени было довольно сложно внедрять на серверной стороне AVIF-изображения, но свежие версии библиотек вроде sharp сильно облегчили эту задачу.

<picture> <source   sizes="(max-width: 608px) 100vw, 608px"   srcset="     /img/Z1s3TKV-1920w.avif 1920w,     /img/Z1s3TKV-1280w.avif 1280w,     /img/Z1s3TKV-640w.avif   640w,     /img/Z1s3TKV-320w.avif   320w   "   type="image/avif" /> <!-- snip lots of other stuff --> <img /></picture>

Загрузка правильного количества пикселей


В приведённом выше коде есть атрибуты srcset и sizes. С помощью селектора w они говорят браузеру, какой нужно брать URL в зависимости от физического количества пикселей, которое потребуется для отрисовки изображения на конкретном устройстве. Это количество зависит от ширины изображения, которая вычисляется на основе атрибута sizes (представляющего собой выражение из медиа-запроса).

Благодаря этому браузер всегда будет загружать наименьшее возможное изображение, обеспечивающее наилучшее качество на конкретном устройстве. Либо он может выбрать самое маленькое изображение, если пользователь включил режим экономии данных.

Запасное решение


Браузерам, которые поддерживают только старые форматы изображений, можно с помощью srcset предоставить больше исходных элементов:

<source sizes="(max-width: 608px) 100vw, 608px" srcset="   /img/Z1s3TKV-1920w.webp 1920w,   /img/Z1s3TKV-1280w.webp 1280w,   /img/Z1s3TKV-640w.webp   640w,   /img/Z1s3TKV-320w.webp   320w " type="image/webp"/><source sizes="(max-width: 608px) 100vw, 608px" srcset="   /img/Z1s3TKV-1920w.jpg 1920w,   /img/Z1s3TKV-1280w.jpg 1280w,   /img/Z1s3TKV-640w.jpg   640w,   /img/Z1s3TKV-320w.jpg   320w " type="image/jpeg"/>

Кеширование и неизменяемые URL


Встраивайте в URL изображения хеш количества байтов, которые занимает картинка. В примере выше я сделал это с помощью Z1s3TKV. При изменении изображения поменяется и URL, а значит вы можете применять бесконечное кеширование картинок. Кеширующие заголовки должны выглядеть так: cache-control: public,max-age=31536000,immutable.

immutable семантически правильное значение cache-control, но сегодня оно мало поддерживается браузерами (я смотрю на тебя, Chrome). max-age=31536000 запасной способ кеширования в течение года. public нужен для того, чтобы ваш CDN кешировала изображение и доставляла его с границы сети. Но этот подход можно использовать только в том случае, если он не нарушает ваших правил соблюдения приватности.

Ленивая загрузка


Добавив loading=lazy в элемент img мы говорим браузеру начать извлекать изображение лишь тогда, когда оно готово быть отрисовано.

<img loading="lazy"  />

Асинхронная расшифровка


Добавив decoding=async в элемент img мы разрешаем браузеру расшифровывать изображение вне основного потока, чтобы эта процедура не помешала пользователю. Заметных недостатков у этого решения быть не должно, за исключением того, что оно не всегда применима по умолчанию в старых браузерах.

<img decoding="async"  />

Размытая заглушка


Размытая заглушка это инлайненное изображение, дающее пользователю какое-то представление о полноценной картинке, которая будет позднее загружена, без передачи данных по сети.

https://www.industrialempathy.com/img/blurry.svg


Несколько замечаний по реализации:

  • Заглушка инлайнится как background-image изображения. Эта методика позволяет отказаться от второго HTML-элемента буквально прячет заглушку, когда загружается основное изображение, для этого не нужен JavaScript.
  • URI данных основного изображения обёртывается в URI данных SVG-картинки. Это делается потому, что размытие выполняется на уровне SVG, а не с помощью CSS-фильтра. То есть размытие выполняется однократно для каждого изображения при растеризации SVG, а не для каждого макета. Это экономит ресурсы процессора.

<img style="          background-size: cover;     background-image:       url('data:image/svg+xml;charset=utf-8,%3Csvg xmlns=\'http%3A//www.w3.org/2000/svg\'xmlns%3Axlink=\'http%3A//www.w3.org/1999/xlink\' viewBox=\'0 0 1280 853\'%3E%3Cfilter id=\'b\' color-interpolation-filters=\'sRGB\'%3E%3CfeGaussianBlur stdDeviation=\'.5\'%3E%3C/feGaussianBlur%3E%3CfeComponentTransfer%3E%3CfeFuncA type=\'discrete\' tableValues=\'1 1\'%3E%3C/feFuncA%3E%3C/feComponentTransfer%3E%3C/filter%3E%3Cimage filter=\'url(%23b)\' x=\'0\' y=\'0\' height=\'100%25\' width=\'100%25\'       xlink%3Ahref=\'data%3Aimage/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAkAAAAGCAIAAACepSOSAAAACXBIWXMAAC4jAAAuIwF4pT92AAAAs0lEQVQI1wGoAFf/AImSoJSer5yjs52ktp2luJuluKOpuJefsoCNowB+kKaOm66grL+krsCnsMGrt8m1u8mzt8OVoLIAhJqzjZ2tnLLLnLHJp7fNmpyjqbPCqLrRjqO7AIeUn5ultaWtt56msaSnroZyY4mBgLq7wY6TmwCRfk2Pf1uzm2WulV+xmV6rmGyQfFm3nWSBcEIAfm46jX1FkH5Djn5AmodGo49MopBLlIRBfG8yj/dfjF5frTUAAAAASUVORK5CYII=\'%3E%3C/image%3E%3C/svg%3E');   " />

(Опциональная) JavaScript-оптимизация


Браузеры могут быть вынуждены растеризовать размытую заглушку, даже если изображение уже загружено. Проблему можно решить, убрав растеризацию при загрузке. Кроме того, если у вашего изображения есть прозрачные области, то эта оптимизация становится обязательной, иначе сквозь картинку будет просвечивать заглушка.

<sсript> document.body.addEventListener(   "load",   (e) => {     if (e.target.tagName != "IMG") {       return;     }     // Remove the blurry placeholder.     e.target.style.backgroundImage = "none";   },   /* capture */ true );</sсript>

Дополнительно


Полезный инструмент, реализующий все описанные оптимизации: eleventy-high-performance-blog
Подробнее..

Веб-империя правительства UK все во имя человека, для блага человека

07.04.2021 08:12:05 | Автор: admin

Цифровизация сейчас в повестке дня едва ли не любого государства. Но все понимают ее по-разному, по-разному ее начинают, до разной степени готовности и продуманности доводят и приоритеты расставляют по-разному.

Для кого-то и у кого-то цифровизация это контроль и запреты, для кого-то и у кого-то удобство и польза каждого отдельного гражданина.

С первым вариантом вы все знакомы очно, а о том, как властям Великобритании удается идти по второму пути рассказываем в этой статье на примере подходов к созданию правительственных сайтов.

СОДЕРЖАНИЕ

Введение

Правительство Её Величества и его Web

Состав

Сайты

Структура и дизайн

Полный Flat

Код, софт, Performance

Палитра

Содержание по-прежнему Queen

Семантика

Atomic дизайн

Информационное сопровождение

Выводы

ПРЕДУПРЕЖДЕНИЕ: немного лонгрид, вынужден сразу предупредить. Но материал задуман и предназначен для других целей, так что короче никак. Поэтому, кому не интересно, не тратьте время, а кому интересно, наберитесь чуточку терпения.

Введение

Всё познается в сравнении. Неважно, кто это сказал, важно, что это справедливо. ( I ) Справедливо для многих вещей, а для некоторых, я подозреваю, едва ли не единственный способ вникнуть в их суть и понять происходящее.

Говоря о хроническом недоюзе социальных сетей российскими органами власти и его последствиях для нас, я вскользь, но намеренно упомянул стандарты доступности сайтов с твердым намерением вернуться к этой темe. ( II )

По большому счету, с учетом 30-летней истории развития Всемирной паутины и всего, что с ней связано, наблюдаемые в России в отношении социальных сетей недоверие, высокомерие, неумение пользоваться, непонимание назначения это лишь малая часть куда более существенной и системной проблемы, а именно все еще первобытного состояния Рувеба и его хронического отставания от наилучших практик и общепринятых стандартов. ( III )

В первую очередь речь о таких составляющих, как безопасность, эффективность, доступность, уровень подготовки кадров, в конце концов, способных обеспечивать решение задач на уровне, соответствующем далеко уже не началу XXI века. Применительно к России, например, мы вынуждены констатировать, что в массе своей уровень подготовки кадров, ответственных по крайней мере за государственный веб, остается ниже всякой критики.

Однако, после некоторого размышления разговоры о доступности и, о как более общем подходе, об ориентированности дизайна веб-сайтов и приложений на пользователя (user-centred design) ( IV ) я решил предварить разговором о дизайне в целом; прежде всего, в ключе его UX/UI составляющей. В конце концов, не будь пользователь изначально поставлен в центр этой инженерной задачи, предваряющей любую разработку, все разговоры о доступности отпадут сами собой.

Взглянуть на проблему можно с двух сторон изнутри и извне. Можно нарисовать вызывающую беспокойство и уныние картину нашей действительности и предоставить читателю самому додумывать, как далеки мы от цивилизации, подразумевая, что он знает, где она и как выглядит. А можно показать ему другие миры, предположив, что о положении дел у себя дома он и без того осведомлен куда лучше, чем о делах у соседей, следовательно, и судить о ширине пропасти между как есть и как надо (или как можно) ему в таком случае будет проще. Полагая, что о хорошем говорить приятнее и полезнее а позитив в наши дни никому не помешает, считаю целесообразным выбрать второй подход.

По ряду соображений отношение именно государственных органов с интернетом, его место в их деятельности, состояние дел с государственным веб-строительством представляют особый интерес. Государство в целом является крупнейшим поставщиком услуг для бизнеса и населения и крупнейшим заказчиком работ, связанных с веб разработкой, и, следовательно, расходователем средств по этой статье. Раз так, справедливо ожидать от него привлечения наиболее подготовленных кадров и следования наилучшим практикам. В случае реализации такого сценария, правомерно ожидать, что проекты, реализованные в этом секторе окажутся в своем роде эталонными.

Чтобы понять, так это или нет, как одни государства выглядят на фоне наилучших практик в этой сфере (а выглядят они, как мы увидим, по-разному), перефразируя химиков, подобное следует сравнивать с подобным. Поэтому в качестве кандидата для анализа и сегодняшнего разговора я выбрал сайты правительства Соединенного королевства (Великобритании и Северной Ирландии). Вопросы о выборе, надеюсь, развеются по ходу изложения. А если нет, то конструктивный диалог в помощь.

Правительство и Web Её величества Today

Состав

Согласно сайту gov.uk на начало апреля 2021 года правительство Соединенного Королевства и имеющие к нему отношение организации, это:

  • офис премьер-министра;

  • 23 министерства;

  • 20 департаментов, не являющихся министерствами;

  • 411 агентств и прочих госструктур;

  • 105 значимые профильные организации;

  • 13 госкорпораций;

  • и 3 автономные администрации.

Итого: 576 ед.

Рисунок 1. Узнать, что такое правительство Соединенного Королевства, предельно просто, вплоть до принадлежащих государству госкорпораций.Рисунок 1. Узнать, что такое правительство Соединенного Королевства, предельно просто, вплоть до принадлежащих государству госкорпораций.

Сайты

Сайты далеко не всех перечисленных на этой странице 576 entities заведены под зонтик gov.uk, многие организации имеют сайты в собственных доменах. Однако, забегая вперед, скажем, что это не освобождает их владельцев от необходимости соблюдения общих требований. Требований же таких немало, и прописаны они на удивление детально и, что тем более немаловажно, исключительно доступным языком. ( V )

Рисунок 2. Чтобы помочь веб-дизайнеру на службе у Её величества делать свою работу хорошо, предусмотрена масса сопроводительной документации.Рисунок 2. Чтобы помочь веб-дизайнеру на службе у Её величества делать свою работу хорошо, предусмотрена масса сопроводительной документации.


Примечательное еще и то, что те сайты, которые таки находятся в домене gov.uk, находятся именно в нем. Не в доменах третьего уровня, как это часто практикуется в подобных ситуациях, а именно в домене gov.uk. Можно сказать, что происходят от одних и тех же родителей и имеют одну и ту же наследственность (см. про используемые технологии ниже). Ссылки на сайты отдельных ведомств имеют следующий вид:

Рисунок 3. Ссылка на сайт целого министерства практически неотличима от ссылки на рядовой HTML-документ. Всё, как Тим Бернерс-Ли и задумывал.Рисунок 3. Ссылка на сайт целого министерства практически неотличима от ссылки на рядовой HTML-документ. Всё, как Тим Бернерс-Ли и задумывал.

Все вместе правительственные сайты в домене gov.uk это полмиллиона страниц, множество документов и услуг и в среднем примерно 5,1 млн посещений ежедневно, не считая тех, кто отказался от приема cookies. ( VI )

Хозяйство, как видите, вполне себе имперских масштабов, анархии и вольнодумства не терпящее.

Структура и дизайн

Полный Flat

Над вопросом с чего начинается Родина в случае с веб-империей правительства Её величества голову ломать совершенно не приходится. Набираете в адресной строке gov.uk, и вы дома.

Рисунок 4. Парадный веб-подъезд правительства Её величества королевы Великобритании.Рисунок 4. Парадный веб-подъезд правительства Её величества королевы Великобритании.

Первое впечатление от посещения: лаконичность, строгость, функциональность. В реализованной концепции все уплощено до предела, ни тени в прямом и переносном смысле, ни на намека на реализм. Можно сказать, gov.uk это квинтэссенция плоского (Flat) дизайна.

Во-первых, полное отсутствие какой бы то ни было движухи. Никаких всплывающих, выпадающих, мелькающих, мигающих элементов на вашем пути. Никаких параллаксов, безразмерных геройских имиджей, упакованных в карусели, ( VII ) словом, ничего такого, без чего многие наши соотечественные дизайнеры жизни себе в профессии не представляют и чем пичкают к месту и не к месту едва ли не любые сайты, независимо от их назначения.

Сейчас еще не время, но когда мы будем говорить о реализации принципов доступности на британских правительственных сайтах согласно действующих стандартов, станет понятно, почему все это важно, равно как и цветовая гамма, о которой речь ниже.

Во-вторых, полное отсутствие незатейливой пропаганды, приторного славословия в чей бы то ни было адрес, другой пустопорожней информации. Никаких программных цитат с обещаниями догнать и перегнать, памятных фото с бесчисленных и бесконечных заседаний (теперь и видеоконференций). Ничего такого, без чего многие госслужащие пресс-служб и информационных отделов, занимающиеся поддержанием госсайтов, также не мыслят свою работу; никаких бесчисленных и бессмысленных баннеров ( VIII ) о всевозможных работающих и неработающих госпрограммах и госпроектах, единственное назначение которых, как может показаться, и не без основания, замедлить работу и без того далеких от совершенства сайтов.

Одним словом, ничего без нужды отягощающего код и представление сайта, ничего лишнего, что могло бы повредить производительности, споткнуться об экзотический браузер или устройство, с которого осуществляется доступ, что могло бы отвлечь драгоценное внимание посетителя от задачи или вопроса, решить которые он пришел на тот или иной правительственный ресурс.

Код, софт и Performance

Раз зашла речь о производительности, необходимо эту тему немного развить.

Согласно Wappalyzer, правительственные сайты, находящиеся в кусте gov.uk, очень неприхотливы в используемых технологиях. За основу взят Ruby on Rails, веб-сервер и обратные прокси поставлены на Nginx, Varnish для кэширования, из JS-библиотек jQuery (1.12.4) и три собственных UI-фреймворка GOV.UK Toolkit, GOV.UK Template и GOV.UK Front-End, и, как говорят англичане, thats it.

Рисунок 5. Уверен, поставь руководство перед британскими разработчиками задачу сварить щи из топора, они бы справились.Рисунок 5. Уверен, поставь руководство перед британскими разработчиками задачу сварить щи из топора, они бы справились.

Такой инструментальный минимализм освобождает веб-страницы от массы ненужного кода, а пользовательские браузеры и правительственные веб-серверы от массы ненужной работы. Всего пара скриптов перед закрывающим </body>-тегом вот и весь JavaScript.

Но все же за все приходится платить. Задействовав jOuery, сайты поплатились малость безопасностью (D в тесте на WebPageTest за безопасность прилетело именно из-за нее; см. рисунки ниже), что совсем не на руку обширному веб-хозяйству gov.uk, так как используемые технологии касаются всех сайтов в домене. Но если говорить о производительности, то тут нетребовательность англичан окупается сторицей. А пользователи, как мы знаем, больше любят сайты быстрые, чем безопасные. ( IX )

Рисунок 6. PageSpeed Insights для десктопов.Рисунок 6. PageSpeed Insights для десктопов.Рисунок 7. PageSpeed Insights для мобильных устройств. На наш взгляд, с оптимизацией типографики британским дизайнерам еще стоило бы поработать.Рисунок 7. PageSpeed Insights для мобильных устройств. На наш взгляд, с оптимизацией типографики британским дизайнерам еще стоило бы поработать.Рисунок 8. WebPageTest (кабельное подключение, браузер Chrome).Рисунок 8. WebPageTest (кабельное подключение, браузер Chrome).

И на этом разговор о технических характеристиках gov.uk можно, пожалуй, закончить. Кто понимает, о чем идет речь, да поймет.

Цветовая палитра

В продолжение разговора о плоском дизайне Выбором инструментов разработки сайта дело не ограничивается. Общий аскетично-анемичный крайне благоприятный для посетителей настрой сайта не был бы таковым вовсе, не найди он продолжение в выборе цветовой гаммы.

Два цвета черный и голубой выбраны словно по заказу, чтобы, с одной стороны, напоминать королевским подданным о неразрывной связи метрополии с морем (и, как всех нас, с небом, конечно) и о британской чопорности, ставшей уже едва ли не характеристической чертой нации, с другой. Но, конечно, не все так тривиально в этой задумке. Вот, скажем, столь популярному красному цвету, который в изобилии на британском флаге и вообще в британской повседневности (телефонные будки, автобусы и еще много чего), на правительственных сайтах места не нашлось.

Часто выбор цветовой гаммы всего проекта диктуют цвета логотипа (герба, эмблемы, флага). Но в данном случае дизайнеры справедливо решили, извините за тавтологию, что это не тот случай. Ничто не должно отвлекать внимание посетителя от основной цели визита. Полагаю, именно этим соображением они и руководствовались, когда конвертировали богатую гамму правительственного герба в черно-белый монохром. Чтобы заодно и от себя отогнать все соблазны позволить излишней пестроте просочиться в серьезные вопросы человеческой психологии.

Рисунок 9. Bye-bye, скевоморфизм: правительственный герб в оригинале и на сайте.Рисунок 9. Bye-bye, скевоморфизм: правительственный герб в оригинале и на сайте.

С точки зрения доступности выбор черного безупречен, с точки зрения внушения доверия и приведения мыслей в порядок голубой полностью оправдан. Никаких случайных порывов дизайнерской мысли, все выверено точно до точки. После белого и черного голубой, пожалуй, самый неконфликтный цвет.

Традиционно голубой цвет считается одним из наиболее любимых у детей. И хотя дети не являются ключевой аудиторией правительственных сайтов, возможно, он также благотворно действует на людей с теми или иными формами аутизма, который, как известно, сопровождает людей на протяжении всей жизни. Как бы там ни было, думаю, большинство согласится, что встань, скажем так, неуравновешенный человек перед выбором (на основе внешнего вида веб-сайта), с кем ему идти в разведку с правительством Великобритании или с ЦРУ (см. новый дизайн сайта ЦРУ), он бы выбрал первое.

И если кто-то возьмет на себя смелость сказать, что задача выбора цветов для сайта (и не только; пожалуй, для любой художественной композиции) не так принципиальна и сложна (важна), дизайнер Томас Кэннон с ним вряд ли согласится. Применительно к собственному опыту он пишет: ...выбор цветовой схемы, которая подойдет тому или иному проекту, для меня всегда был трудной задачей; это, возможно, одни из самых сложных решений, которые мне как дизайнеру приходится принимать.

Одно небольшое отступление от двух цветов дизайнеры все же сделали. Но оно настолько несущественно, настолько не броско, касается только министерских сайтов (их 23), что нисколько не противоречит генеральной линии и не нарушает никаких канонов.

Содержание все еще Queen

Абстрагируясь от конкретных используемых и неиспользуемых в создании британских правительственных сайтов подходов и технологий, главное, что отличает и сам процесс их создания и, как следствие, конечный продукт это тщательная скрупулезная продуманность и согласованность (consistency).

Ни на главной странице портала, ни на страницах отдельных министерств и ведомств нет ничего лишнего и случайного. Ни одна составляющая, ни один этап не принесены в угоду другим, не задвинуты на задний план, все выверено и сбалансировано.

И это отдельная заслуга дизайнерской команды и далеко не само собой разумеющееся, поскольку сами по себе ни выбор вида дизайна, ни упрощение цветовой гаммы, ни сведение к минимуму задействованных при разработке программных средств и решений не служат гарантией того, что безупречные UX/UI будут вам теперь обеспечены автоматически.

Содержание (информация, за которой приходят на сайты люди) по-прежнему король (применительно ко времени и месту все же королева, пожалуй), и, как заверяет нас Forbes, останется таковым навсегда. Поэтому пора посмотреть, из чего же, из чего же сделаны эти страницы на gov.uk?

Первым делом, первым делом человек!

Хедер

В самом верху в левой части белым по черному размещен логотип стилизованная корона и надпись GOV.UK. Под логотипом также на черном фоне через голубую полосу размещены две ссылки на темы коронавирус и Brexit, с которыми, судя по акценту, да простится мне ирония, по мнению правительства каждый британец должен отходить ко сну и начинать свой день . (На двух изображениях ниже рамки желтым даны в иллюстративных целях, в оригинале они отсутствуют.)

Рисунок 10. Для многих российских дизайнеров столь нерачительное обращение со свободным пространством едва ли не святотатство. Хотя есть и обратные примеры (см. сноску VII в конце статьи).Рисунок 10. Для многих российских дизайнеров столь нерачительное обращение со свободным пространством едва ли не святотатство. Хотя есть и обратные примеры (см. сноску VII в конце статьи).

Собственно, это хедер, и в таком неизменном виде вы теперь встретите его на сайте любого министерства и ведомства, размещенного в данном домене. Единственное, поле для поиска, которое сейчас на месте привычного нам геройского изображения, будет перемещено в правый верхний угол и встанет на одном уровне с логотипом. Вот так:

Рисунок 11. ...и снова: в отличие от многих наших соотечественников британским дизайнерам в голову не могло прийти заставить пользователя мотаться по сайту в поисках поля для поиска, прежде чем дать ему возможность им воспользоваться (иллюстрации к иронии см. в конце статьи (в рамке)).Рисунок 11. ...и снова: в отличие от многих наших соотечественников британским дизайнерам в голову не могло прийти заставить пользователя мотаться по сайту в поисках поля для поиска, прежде чем дать ему возможность им воспользоваться (иллюстрации к иронии см. в конце статьи (в рамке)).

И это все. Никаких изображений индивидуальных оптических приборов, разновеликих букв, которые людям с плохим зрением ни большие, ни маленькие все равно не видны; никаких перетычек на другой язык (в самом деле, зачем вам другой, кроме английского, если вы хотите иметь дело с правительством Соединенного королевства?!); ( X ) наконец никаких привычных глазу в каком бы то ни было виде меню и прочих элементов, коими у нас, да и не только принято обильно забивать шапку сайта, словно это, выражаясь словами одного ну о-очень известного англичанина пустой чердак, куда можно набить все, что угодно.

Кстати, темная тема для хедера по сравнению с остальными элементами страницы определенно не случайный выбор. Хотите, чтобы посетитель расслабился и переключил внимание на содержание страницы, темная тема будет в кассу.

Тело

Сразу под хедером, задача которого не отвлечь на себя, а переключить внимание посетителя, вместо совершенно не несущего в подавляющем большинстве случаев никакой смысловой и информационной нагрузки дежурного изображения (тем более целой обоймы таковых, прикованных одной цепью к карусели) вас встречает приветствие с пояснением, что вы попали в лучшее для поиска правительственной информации и услуг место. Для пущей убедительности для наиболее сомневающихся еще ниже почти в олимпийском стиле следует: Проще, доступнее, быстрее. И далее под текстом, ожидаемо, идет поле для ввода поисковых запросов.

Это не просто задвинутое или забытое на обочине сайта неприметное увеличительное стеклышко единственное напоминание, что на сайте функция поиска все же присутствует, и единственное предназначение которого игра в прятки с пользователем: Ты меня сначала отыщи, а там и я тебе послужу, может быть. Мимо этого поля для поиска нельзя пройти и не заметить его тоже невозможно. На конкретных сайтах, как мы уже говорили выше, оно переедет на еще более заметное место.

Вынести функцию поиска на самое видно место на информационном сайте было более чем разумно. Как, скажем, в аэропорту мы первым делом ожидаем увидеть информационное табло и информационную стойку, а не буфет с кофе и пивом по заоблачным ценам, так и на госсайте логично позволить посетителю сразу перейти к поиску необходимой информации или услуги, а не испытывать его терпение, заставляя разглядывать одни и те же физиономии в разных ракурсах (теперь еще и в масках). Хорошо, хоть британцы это понимают (или им физиономии свои меньше нравятся?).

(Тем более, в самом деле, трафик госсайтам не нужен; кто пришел за услугой и информацией, без нее не уйдет, так не лучше ли сделать так, чтобы на входе скапливалось как можно меньше народа? сделать наоборот, примерно то же самое, как перед входом на эскалаторы метро в Рождество устраивать распродажи).

На всякий случай справа от поиска даны несколько ссылок на наиболее популярные у посетителей темы: Посмотри, вдруг и искать ничего не придется. По всему видно, правительство Ее величества спит и видит, как снабдить ее подданных необходимой информацией наиболее эффективным и не затратным ни для кого способом.

Следом идет блок ссылок (и все это еще до сгиба!), детализирующий задачи и функции, осуществляемые правительством. Они сгруппированы в 16 разделов, в частности: занятость, гражданство и проживание в UK, преступность и правосудие, деньги и налоги, образование, визы и иммиграция и т.д.

Таким образом, если посетителя интересует получение водительских прав, ему не приходится гадать, к ведению какого ведомства отнесена данная услуга. Логика предоставления информации исходит из принципа не важно, кто (окажет услугу), важно, что (в чем заключается потребность конкретного человека) и как.

Следующий раздел всего пять ссылок в трех абзацах по две строчки каждый представляет посетителю несколько другой информационный ракурс. Здесь он может узнать, что собой представляет правительство, сколько в нем каких подразделений, как они называются, как попасть на их сайты (см. Рисунок 1). Что примечательно, никаких промежуточных визитных карточек не предусмотрено; нажав на ссылку, вы сразу переходите на интересующий вас сайт.

Здесь же ссылка на страницу, где собраны все правительственные новости и сообщения, статистические данные, рекомендации по различным вопросам и очень важный раздел, где до сведения посетителя доводится информация о том, насколько эффективно предоставляются государственные услуги, а также предоставляется возможность для обратной связи.

Несколько метрик используется для оценки эффективности предоставления госуслуг, в частности: стоимость трансакции, процент людей, успешно воспользовавшихся той или иной услугой, уровень удовлетворенности пользователей (по оценке самих же пользователей, методом опроса) и доля пользователей, воспользовавшихся цифровой услугой в сравнении с другими доступными вариантами ее предоставления. Там же ссылки на подробные методики составления и использования этих метрик и на сервисные панели (всего 777 ( XI ) + еще 15).

В целом главная страница сверстана так, чтобы дать посетителю все необходимое для начала дальнейшего пути, если он начал его именно с нее, и ни на минуту не отвлечь его внимание менее существенной информацией. Можно сказать, что это идеальный лэндинг вместе с воронкой одновременно. Если посетитель забрел на сайт не случайно, у него просто нет иного шанса, как не быть в нее затянутым.

И если мотивом создания такой страницы были те же соображения, о которых битых полтора часа талдычат в Социальной дилемме, то можно только восхититься, насколько ненавязчиво и элегантно это сделано. И, конечно, следует отдать должное правительству за такой человекоориентированный подход, отдавая себе отчет (нам ли не знать!), что монополист а любое правительство, вне всякого сомнения, монополист в части отправляемых им функций и предлагаемых услуг мог бы позволить себе отмахнуться от такой малости, как удобство своих клиентов все равно придут и все равно купят. Как оказывается, не в нашем, вернее, не в британском случае.

Футер

Футер последний элемент на который непременно следует обратить внимание. Условно футер правительственных сайтов состоит из двух версий минимальной и полной.

Рисунок 12. Все, что мы привыкли видеть в футере, также отсутствует. И неслучайно.Рисунок 12. Все, что мы привыкли видеть в футере, также отсутствует. И неслучайно.

Полная в своей верхней части практически полностью повторяет информационный блок с главной страницы, который идет сразу после хедера и приветствия с поисковой строкой. К нему лишь добавлены третьей колонкой еще ряд важных по мнению архитекторов портала ссылок. Это очень удачный ход: оставить посетителю напоминание о главной странице и не заставлять его при необходимости каждый раз бегать туда-сюда-обратно. А так, где бы он ни был, главная страница всегда с ним. С другой стороны, на сайте какого бы ведомства пользователь ни был, если он нажмет на логотип в самом верху хедера, он попадет именно на главную страницу портала gov.uk.

Минимальная версия футера самая нижняя часть сайта. Здесь собраны ссылки служебного характера (помощь, куки, заявление о доступности, ответственная за создание и поддержание сайта организация и т.п.), герб и информация об авторских правах и используемой лицензии.

Полная версия используется на всех официальных правительственных сайтах в домене gov.uk; минимальную можно встретить на прочих сайтах, относящихся к правительству, но не являющихся сайтами правительственных структур. В частности, это блоги, которые размещены в домене третьего уровня blog.gov.uk в виде отдельных сайтов четвертого уровня (например, accessibility.blog.gov.uk); сервисная информация, предоставляемая на сайте третьего уровня data.gov.uk и т.п.

Еще раз посмотрим внимательно на футер. Ни привычной контактной информации, ни ссылок на используемые соцсети в каком бы то ни было виде. Как так?

На самом деле, все очень просто. Помещать контактную информацию и ссылки на соцсети в футер имеет смысл, когда речь идет о сайте одной организации. Но здесь сайтов море, и грузить всю эту информацию в футер означало бы то же самое, что делать его для каждого сайта заново. А это столько лишней работы! Поэтому вся индивидуальная информация, имеющая отношение к конкретным организациям, из футера выделена, оставлено только то, что имеет отношение ко всем.

Это тем более имеет смысл, так как раздел контактов у многих правительственных организаций может оказаться очень обширным. Британские ведомства, в отличие от многих наших, не стесняются предоставлять своим посетителям как можно больше вариантов и возможностей для связи с ними. За примером сходите на сайт Форин Офис (FCDO) видели?.. сами можете себе представить, каким должен быть футер, чтобы вместить в себя всю эту информацию.

Что еще столь привычное глазу серфера российских сайтов отсутствует в британском футере, не догадываетесь? Ну, конечно же! Баннеры бесчисленных датчиков, счетчиков, виджетов и прочей дребедени, столь любимой российскими разработчиками. Представляете, от скольких тысяч тонн словесной кодовой руды (простите, Владимир Владимирович!) разом удалось избавиться британцам, выметя весь этот <script>-сор из избы? Представляете ту одышку, которая неизбежно ждала их сайты, дай они им обрасти всем этим паразитным балластом?

И уж тем более ни в сказке сказать, ни пером описать, как такая чистота глаз простого посетителя радует. Не футер, а чисто английский газон, чесс слово!

Структура отдельных сайтов

Этот раздел не займет много места. По большому счету, мы уже все обсудили. Но взглянуть на устройство индивидуальных сайтов все же стоит.

В качестве примера можно взять любой. Например, сайт GDS (Government Digital Service), организации, ответственной за все это веб-хозяйство (ссылка на ее сайт в футере). Выбор абсолютно не принципиален, поскольку, какой бы сайт вы из этого массива ни вытащили, все они будут иметь одну и ту же структуру (см. рисунок 13).

Рисунок 13. Все правительственные сайты в домене gov.uk имеют одну и ту же структуру.Рисунок 13. Все правительственные сайты в домене gov.uk имеют одну и ту же структуру.


Единообразие в организации сайтов позволяет избавиться от меню, без которого многие вообще не мыслят себе хороший сайт, а его отсутствие считают моветоном. Оказывается, нет ничего невозможного!

Карта сайта тоже становится не очень нужна. Когда и без того все разложено по полочкам, плюс продуманная функция поиска, зачем ненужные лишние бирки и узелки на память?

Еще один момент, на который непременно стоит обратить внимание. Это элемент, предваряющий футер.

Рисунок 14. Еще одна кнопка к интерфейсу, и как дизайнер на будущее вы экономите себе массу времени и сил. А государству денег, естественно.Рисунок 14. Еще одна кнопка к интерфейсу, и как дизайнер на будущее вы экономите себе массу времени и сил. А государству денег, естественно.

Для UX/UI дизайнера это самая важная закладочка. Принципиальное отличие британского варианта от массы других в наличии третьей кнопки. В то время как одни позволяют посетителю лишь выразить свое мнение на предмет полезности предоставленной им информации, британцев действительно интересует его опыт взаимодействия с ресурсом. Отсюда на случай, если что-то пошло не так, третья кнопка и возможность для посетителя высказать все, что он думает.

Те, кто действительно заморачивается на пользовательском опыте и кто знает, какую роль в совершенствовании любого продукта занимают исследование и анализ взаимодействия пользователя с ним, поймут, какой в этом смысл.

Часто выяснение причины, почему от той или иной страницы посетители отскакивают, как мячики, доставляет реальную головную боль. И когда разворот в боевые порядки всей гугл аналитики и сопутствующего аналитического инструментария маячит как неизбежность, ты начинаешь понимать, как кстати сейчас пришлись хотя бы пара-тройка конструктивных комментариев от живых, а не смоделированных personas.

И то, что мы видим у британцев, наводит на мысль, что эти ребята в своем деле толк знают.

Atomic дизайн

Туго пришлось бы британским госдизайнером, скорее всего не случилось бы им создать свое веб-чудо, не будь они знакомы с концепцией atomic ( XII ) дизайна от Брэда Фроста. ( XIII ) Она позволила добиться той лаконичности и содержательности одновременно, которые иным UX/UI дизайнерам не снятся даже в самых смелых фантазиях. Пойди они проторенной дорожкой, и обрекли бы себя на поистине сизифов труд. Не помогли бы ни минимализм, ни собственные фреймворки.

Унификация и последовательность позволили наперед сэкономить массу сил и средств не только создателям и владельцам сайтов, но, что куда более важно и масштабно, их пользователям.

Теперь любой сайт для любого посетителя, побывавшего хоть раз на одном из них, что дом родной. Нет больше необходимости привыкать и обживаться на каждом из них снова и снова, случись ему посетить несколько сайтов.

Теперь какого бы ведомства сайт ему ни пришлось посетить, он не будет чувствовать, что каждый раз ступает на неизведанную территорию. Исчезла необходимость каждый раз с нуля разбираться, что на нем где и как устроено, и есть ли вообще надежда найти искомое.

Теперь сэкономленные на этой рутине минуты, сливающиеся в часы, дни, месяцы и годы, подданные Её величества могут с толком потратить на милые их душам и сердцам занятия, на то, чтобы сделать свою жизнь еще лучше.

И это убедительное доказательство эффективности задействованного подхода, ориентированного на пользователя, его очевидной экономической целесообразности, результаты которого легко поддаются измерению и оценке.

We Want Information, Information, Information

Блоги

We Want Information, Information, Information, такими словами начинается композиция The Prisoner из альбома The Number of the Beast группы Iron Maiden (британской, конечно же!). И, знаете, власти совсем не против: Хотите информацию? Пожалуйста!

Чтобы поддерживать все это хозяйство в надлежащем виде, от всех сотрудников, причастных к процессу, требуется слаженность и последовательность действий. Один из залогов успешности и бесперебойности этой работы информированность заинтересованных в ней сторон обо всех ее аспектах. Существенную роль в обеспечении такой информированности играют официальные правительственные блоги.

Блоги это то, где значительная часть людей, которые вообще что-то когда-то пишут, делится своей информацией с городом и миром. Блог это удобно, это просто, это эффективно.

Их ведут университеты и их подразделения, исследовательские организации, библиотеки и think tanks, международные организации (World Bank, IMF) и ТНК (CISCO, Microsoft).

Да, далеко не везде вести блог считается зазорным и вообще выше своего достоинства. И вполне естественно, что наряду с гражданами, бизнесом, некоммерческими организациями правительства многих стран также решили не изобретать велосипед и тоже развести свои блогопарки. И правительство Великобритании не исключение.

Всего их у него на сегодняшний день не удивляйтесь, в хорошем хозяйстве все посчитано 137. Собраны все блоги под своим, так скажем, подзонтиком в домене https://www.blog.gov.uk/. Не все из них активны, некоторые переведены в архив, но доступ к ним при этом сохраняется в полном объеме.

Исходя из их количества, можно догадаться, что не все ведомства и службы имеют собственные блоги, зато у некоторых их даже не по одному. Так, у офиса премьер-министра три, у министерства юстиции два, ( XIV ) а у GDS (Government Digital Service), в ведении которой и находятся все правительственные сайты, блогов аж девять.

Но блогами этой службы темы функциональности интересующих нас сайтов и их доступности отнюдь не исчерпываются. Например, Civil Service ведет блог Accessibility in government, а Home Office Home Office Digital, Data and Technology. Следует признать, цифровая тема очень плотно пронизывает неформальную информационную повестку британских государственных служащих. 25 блогов из 137 так или иначе связаны с темой цифровизации. Наберите на главной странице блогов в левой части в раскрывающемся списке ключевое слово digital, и вы сами все увидите.

Руководства и нормативно-правовая база

Для доведения своей официальной позиции по тому или иному вопросу у правительства Великобритании предусмотрены другие каналы. Законодательство собрано на сайте https://www.legislation.gov.uk/, а для различных руководств и разного рода наставлений есть раздел Guidance and regulation. Это на случай, если кто-то уже ухватился за мысль, что блоги это единственный способ общения правительства с гражданами и собственным персоналом.

Тема организации информационного обмена и диалога между властью и обществом исключительно важна с точки зрения формирования доверительных отношений между ними, но все же выходит за и без того широкие рамки UX/UIдизайна. Основной посыл этого раздела в том, чтобы показать, что британские власти готовы идти и идут в вопросе установления и поддержания диалога куда дальше того минимума, на котором другие предпочитают остановиться. Другими словами, предпочитают слушать, что думают люди по тому или иному поводу, а не додумывать и решать за них, что им делать и как им жить.

Выводы

Британский правительственный веб это торжество английского дизайна, это элегантность Роллс-Ройса и практичность Лэнд Ровер Дефендера. Это очередное подтверждение справедливости народных пословиц о том, что прежде чем резать, стоит семь раз отмерить, или что не все то золото, что блестит, и очередное доказательство состоятельности по-государственному гуманистического подхода к нуждам и запросам людей.

Правильно расставленные приоритеты, забота о своих stakeholders, совмещенная с ответственностью перед ними же (если прямым текстом, перед налогоплательщиками, если еще прямее, за расходуемый бюджет), умноженные на компетенции, дали в результате то, что мы открыли для себя в британском правительственном вебе.

Используемые технологии и инструментарий оказываются не так уж и важны. Что важно? Идея, глубина ее проработки, ее архитектура и дизайн, заложенная в нее логика, здравый смысл и постоянное напоминание себе, зачем и ради кого все это делается. Словом, все то, что формирует какой-то особый склад ума, до такой степени культурный, что не любая почва и климат оказываются для него пригодны.

И кто-то, возможно, сочтет вовсе иронией судьбы, что наиболее подходящим местом для такой ментальности оказывается бывшая империя и до сих пор еще монархия, которой не на словах, а не деле удается воплощать в жизнь вынесенный в заголовок статьи лозунг, родившийся 60 лет назад совсем в другой стране.

Из серии Перлы UX/UI дизайна

Любите тесты и квизы? Поищите на досуге функции поиска на сайте Рособрнадзора и/или ФМБА.


(I) Говорят, что это был Рене Декарт.

(II) Чтобы на фоне непрекращающихся разговоров о цифровизации, национальных приоритетах и т.п. более точно определить положение российских ресурсов в контексте следования наилучшим практикам и действующим общепринятым стандартам.

(III) Можно, конечно, продолжать их не замечать, но дальновидно ли это?

(IV) Приводится в английском варианте написания.

(V) Использование доступного среднестатистическому пользователю языка один из главных принципов UCD (user-centered design). И не только в продукте, если он подразумевает его [языка] использование, но и в сопроводительной документации к нему. GDPR, например, требует соблюдения этого принципа и в части правовой информации, размещаемой на сайтах и предназначенной для конечных пользователей. Поэтому, когда наталкиваешься на подобные документы российских ведомств (https://digital.gov.ru/ru/documents/4820/), начинаешь понимать актуальность таких требований, и что возникают они не на пустом месте.

(VI) Теперь в цивилизованных странах с этим строго: не хочешь cookies, не ешь.

(VII) Зайдите на сайт Рослесхоза, и вы увидите, до какого абсурда можно довести идею с hero-изображениями и каруселями. Представьте, вам каждое утро приносят газету, у которой первая страница до сгиба пустая? Сайт Рослесхоза именно такая газета.

(VIII) Почему бессмысленных? Потому что никто не знает, по какому принципу они каждым сайтом отбираются, сколько их вообще существует и почему нельзя просто оформить эту информацию списком (на самом деле можно и нужно). Информация, отраженная на этих баннерах, находится за пределами функции поиска, таким образом они не более чем бутафория, отвлекающая внимание и создающая дополнительную нагрузку на каналы, серверы и на конечные устройства пользователей.

(IX) К тому же ресурсы, имеющие своей целью исключительно предоставление информации, не столь категоричны в вопросах безопасности. Масса российских госсайтов, в том числе президентский и правительственный до сих пор работают через незащищенный http.

(X) Хотя государственный язык в России русский, для нас привычное дело назвать официальный сайт органа государственной власти на английском. В качестве примеров можно привести сайты правительства РФ, Совета Федерации, Росрыболовства и целый ряд других.

(XI) 777, Карл! Ты не поверишь!

(XII) Сказать атомарный, как подается в переводах, просто язык не поворачивается. Это из той же области, что и аккаунт, кешбэк или транзакция.

(XIII) Не сказать, чтобы идея принципиально нова. Автомобилестроители, например, давно собирают машины из агрегатов общих для целой линейки моделей. Да Брэд Фрост и сам с этого начинает. Но же это маркетинг, сами понимаете. Он живет по своим законам.

(XIV) Это очень демократично и прогрессивно по сравнению с российским минюстом, который даже соцсетями пользоваться не удосуживается, https://github.com/shydata/govnets/blob/main/govnetsreport-2020-alexshy-FULLREPORT.pdf.

Подробнее..

Голосовая аналитика бесплатно. Что? Где? Когда?

22.12.2020 18:11:09 | Автор: admin
Большая часть продаж и поддержки все так же происходит по телефону, и во времена удаленки эта цифра только возрастает. Но как контролировать сотрудников колл-центра? Специально для этого и существует голосовая аналитика.
Как она работает, как пользоваться, и как попробовать бесплатно, мы расскажем ниже.




Что такое голосовая аналитика?


Голосовая аналитика комплексное решение анализа речи. Нейросети переводят разговоры из голоса в текст, затем текст анализируется по нескольким параметрам. В частности, инструмент умеет анализировать:

  • скорость разговора,
  • перебивание,
  • молчание,
  • искать ключевые слова,
  • выставлять сводную оценку каждому разговору.


Кому пригодится?


Тем у кого больше одного сотрудника, кто разговаривает по телефону (продажи/поддержка/опросы что угодно).
Слушать все разговоры невозможно. Благодаря автоматическому скорингу, все плохие(или наоборот) разговоры вы сразу же можете прочитать или прослушать в интерфейсе личного кабинета.

Словари и ключевые слова


Инструмент речевой аналитики Zadarma прошел этап альфа-тестирования и мы можем сказать, что чаще всего клиенты используют словари и ключевые слова. В разговоре были употреблены слова из дефолтного словаря (например, использование слов-паразитов оператором), либо из созданного словаря. Руководитель узнает об этом, сможет прочитать расшифрованный текст или прослушать разговор.

Чтобы не тратить время, можно анализировать употребление ключевых слов только на одном канале (у клиента либо сотрудника). Например, если ищете слова-паразиты в разговоре, то не важно, употребляет ли их клиент. Но если менеджер или оператор часто использует ну, как бы, проще говоря, то это лишний повод задуматься. Вот некоторые примеры словарей или ключевых слов:

Слова оператора
Неуверенная работа оператора не знаю, не уверен, не могу помочь
Скрипты покупка совершена, ваш заказ принят в обработку, спасибо за покупку

Слова клиента
Плохое качество связи не слышно, мямлите, внятнее, громче
Недовольство клиента себе позволяете, разговаривайте корректно, хамство, не хамите
Повторное обращение я уже звонил, звонил с другого номера, только что говорил, только что разговаривал
Нецензурная лексика обойдемся без примеров

Слова оператора и клиента
Упоминание конкурентов Кола, пепси, спрайт, миринда, вятский квас
Ошибки, баги сбой, не работает, техническая ошибка, неполадки, зависло

Также система может строить отчеты по дополнительным параметрам: молчание, перебивание (в % эквиваленте), скорость речи, соотношение речи оператора и клиента.
Пример такого отчета:



Сколько стоит и как попробовать?


Сам инструмент речевой аналитики абсолютно бесплатный. Это не первый наш бесплатный инструмент (например бесплатная АТС, коллтрекинг, CRM, виджеты). Платить нужно только за минуты распознавания речи.
Инструмент умеет работать с 50+ языками, и стоимость зависит от языка.
Стоимость распознавания популярных языков, в том числе и русского 90 копеек за минуту разговора.



До 15 января 2021 года мы добавили подарочные минуты для трех тарифов:
  • Стандарт 100 минут бесплатного распознавания (действует только до 15 января)
  • Офис 500 минут бесплатного распознавания (после акции 100 минут)
  • Корпорация 1000 минут бесплатного распознавания (после акции 200 минут)


Для того, чтобы протестировать речевую аналитику:
  1. зарегистрируйтесь в сервисе
  2. подключите бесплатную АТС и виртуальный номер
  3. активируйте распознавание всех разговоров на одном или нескольких внутренних номерах АТС.
  4. далее создайте параметры отчета разговора в разделе Распознавание речи.


P.S. Все должно совершенствоваться. Так как мы предложили голосовую аналитику лишь несколько дней назад, то активно наращиваем ее функционал. Все пожелания по развитию голосовой аналитики с радостью ждем в комментариях.
Подробнее..

Перевод Компилятор всё оптимизирует? Ну уж нет

17.06.2021 12:20:28 | Автор: admin
Многие программисты считают, что компиляторы это волшебные чёрные ящики, на вход в которые можно подать хаотичный код, а на выходе получить красивый оптимизированный двоичный файл. Доморощенные философы часто начинают рассуждать о том, какие фишки языка или флаги компилятора следует использовать, чтобы раскрыть всю мощь магии компилятора. Если вы когда-нибудь видели кодовую базу GCC, то и в самом деле могли поверить, что он выполняет какие-то волшебные оптимизации, пришедшие к нам из иных миров.

Тем не менее, если вы проанализируете результаты работы компиляторов, то узнаете, что они не очень-то хорошо справляются с оптимизацией вашего кода. Не потому, что пишущие их люди не знают, как генерировать эффективные команды, а просто потому, что компиляторы способны принимать решения только в очень малой части пространства задач. [В своём докладе Data Oriented Design (2014 год) Майк Эктон сообщил, что в проанализированном фрагменте кода компилятор теоретически может оптимизировать лишь 10% задачи, а 90% он оптимизировать не имеет никакой возможности. Если бы вам интересно было узнать больше о памяти, то стоит прочитать статью What every programmer should know about memory. Если вам любопытно, какое количество тактов тратят конкретные команды процессора, то изучите таблицы команд процессоров]

Чтобы понять, почему волшебные оптимизации компилятора не ускорят ваше ПО, нужно вернуться назад во времени, к той эпохе, когда по Земле ещё бродили динозавры, а процессоры были чрезвычайно медленными. На графике ниже показаны относительные производительности процессоров и памяти в разные годы (1980-2010 гг.). [Информация взята из статьи Pitfalls of object oriented programming Тони Альбрехта (2009 год), слайд 17. Также можно посмотреть его видео
(2017 год) на ту же тему.]


Проблема, демонстрируемая этим графиком, заключается в том, что производительность процессоров за эти годы значительно выросла (ось Y имеет логарифмический масштаб), а производительность памяти росла гораздо меньшими темпами:

  • В 1980 году задержка ОЗУ составляла примерно 1 такт процессора
  • В 2010 году задержка ОЗУ составляла примерно 400 тактов процессора

Ну и что же? Мы тратим чуть больше тактов на загрузку из памяти, но компьютеры всё равно намного быстрее, чем раньше. Какая разница, сколько тактов мы тратим?

Ну, как ни грустно осознавать, но даже если у нас будут более качественные компиляторы и мощное железо, скорость ПО значительно не повысится, потому что ПО такое медленное не из-за них. Главная современная проблема использование процессоров не во всю их мощь.

В таблице ниже указаны параметры задержки самых распространённых операций. [Таблица взята из книги Systems Performance: Enterprise and the cloud (2nd Edition 2020).] В столбце Задержка в масштабе указана задержка в значениях, которые проще понимать людям.

Событие Задержка Задержка в масштабе
1 такт ЦП 0,3 нс 1 с
Доступ к кэшу L1 0,9 нс 3 с
Доступ к кэшу L2 3 нс 10 с
Доступ к кэшу L3 10 нс 33 с
Доступ к основной памяти 100 нс 6 мин
Ввод-вывод SSD 10-100 мкс 9-90 ч
Ввод-вывод жёсткого диска 1-10 мс 1-12 месяцев

Посмотрев на столбец задержек в масштабе, мы быстро поймём, что доступ к памяти затратен, и что в случае подавляющего большинства приложений процессор просто бездельничает, ожидая ответа от памяти. [Узким местом не всегда является память. Если вы записываете или считываете много данных, то узким местом, скорее всего, будет жёсткий диск. Если вы рендерите большой объём данных на экране, то узким местом может стать GPU.]

На то есть две причины:

  1. Языки программирования, которые мы используем и сегодня, создавались во времена, когда процессоры были медленными, а задержки памяти не были такими критичными.
  2. Best practices отрасли по-прежнему связаны с объектно-ориентированным программированием, которое показывает на современном оборудовании не очень высокую производительность.

Языки программирования


Язык Время создания
C 1975 год
C++ 1985 год
Python 1989 год
Java 1995 год
Javascript 1995 год
Ruby 1995 год
C# 2002 год

Перечисленные выше языки программирования придуманы более 20 лет назад, и принятые их разработчиками проектные решения, например, глобальная блокировка интерпретатора Python или философия Java всё это объекты, в современном мире неразумны. [Все мы знаем, какой бардак представляет собой C++. И да, успокойтесь, я знаю, что в списке нет вашего любимого нишевого языка, а C# всего 19 лет.] Оборудование подверглось огромным изменениям, у процессоров появились кэши и многоядерность, однако языки программирования по-прежнему основаны на идеях, которые уже не истинны.

Большинство современных языков программирования стремится упростить жизнь разработчика, избавив его от мучений с управлением памятью вручную. Хотя отсутствие необходимости думать о памяти делает разработчика продуктивнее, за эту продуктивность приходится платить высокую цену.

Выделение блоков памяти без контроля будет замедлять вашу программу, в основном из-за операций произвольного доступа к памяти (промахов кэша), каждая из которых будет стоить вам сотни тактов процессора. Тем не менее, большинство из перечисленных выше языков программирования всё равно будет вести себя так, как будто выделение произвольной памяти это несерьёзная проблема, и вам не стоит из-за неё беспокоиться, ведь компьютеры очень быстры.

Да, компьютеры чрезвычайно быстры, но только если вы пишете ПО таким образом, что оно хорошо взаимодействует с железом. На одном и том же оборудовании вы может работать и очень плавная 3D-игра и заметно лагающий MS Word. Очевидно, что проблема здесь не в оборудовании и что мы можем выжать из него гораздо больше, чем среднестатистическое приложение.

Совершенствовалось оборудование, но не языки


Ещё одна проблема современных языков программирования в том, что они недалеко ушли от программирования в стиле C. Не скажу, что есть что-то плохое в написании циклов for, но становится всё сложнее и сложнее использовать весь потенциал процессоров и в то же время управлять сложностью современных программ.

Отчасти языкам не хватает функциональности, которая бы позволила писать программы удобным человеку образом и при этом использовать оборудование так, чтобы обеспечивать наилучшую производительность. Хотя используемые нами сегодня типы абстракций удобны, они обычно демонстрируют плохую производительность на современном оборудовании.

Объяснение будет долгим, но давайте начнём с примера:

Представьте, что мы симулируем колонию муравьёв. Административный отдел колонии был уничтожен атакой муравьеда, поэтому он не знает, сколько муравьёв-воинов осталось живо в колонии.

Поможем нашему муравью-администратору посчитать муравьёв-воинов!

Вот пример того, как большинство программистов, включая и меня, писало бы код решения этой задачи. Он написан на типичном объектно-ориентированном энтерпрайзном уничтожителе кэша:

class Ant {    public String name = "unknownAnt";    public String color = "red";    public boolean isWarrior = false;    public int age = 0;}// shh, it's a tiny ant colonyList<Ant> antColony = new ArrayList<>(100);// fill the colony with ants// count the warrior antslong numOfWarriors = 0;for (Ant ant : antColony) {    if (ant.isWarrior) {         numOfWarriors++;    }}

Представленное решение компактно и понятно. К сожалению, на современном оборудовании производительность его невысока.

Каждый раз, когда мы запрашиваем байт памяти, отсутствующий в одном из кэшей процессора, из основной памяти запрашивается вся строка кэша, даже если нам нужен только 1 байт. Так как получение данных из основной памяти довольно затратная операция (см. выше таблицу задержек), нам бы хотелось, чтобы количество таких операций было минимальным. Этого можно достичь двумя способами:

  1. Уменьшив объём данных, которые нужно получать для нашей задачи.
  2. Храня необходимые данные в соседних блоках, чтобы полностью использовать строки кэша.

В приведённом выше примере мы будем считать, что из памяти запрашиваются следующие данные (я предполагаю, что используются compressed oops; поправьте меня, если это не так):

+ 4 байта на ссылку имени
+ 4 байта на ссылку цвета
+ 1 байт на флаг воина
+ 3 байта заполнителя
+ 4 байта на integer возраста
+ 8 байт на заголовки класса
---------------------------------
24 байта на каждый экземпляр муравья


Сколько раз нам нужно обратиться к основной памяти, чтобы подсчитать всех муравьёв-воинов (предполагая, что данные колонии муравьёв ещё не загружены в кэш)?

Если учесть, что в современных процессорах строка кэша имеет размер 64 байта, то мы можем получать не больше 2,6 экземпляра муравьёв на строку кэша. Так как этот пример написан на языке Java, в котором всё это объекты, находящиеся где-то в куче, то мы знаем, что экземпляры муравьёв могут находиться в разных строках кэша. [Если распределить все экземпляры одновременно, один за другим, то есть вероятность, что они будут расположены один за другим и в куче, что ускорит итерации. В общем случае лучше всего заранее распределить все данные при запуске, чтобы экземпляры не разбросало по всей куче, однако если вы работаете с managed-языком, то сложно будет понять, что сделают сборщики мусора в фоновом режиме. Например, JVM-разработчики утверждают, что распределение мелких объектов и отмена распределения сразу после их использования обеспечивает бОльшую производительность, чем хранение пула заранее распределённых объектов. Причина этого в принципах работы сборщиков мусора, учитывающих поколения объектов.]

В наихудшем случае экземпляры муравьёв не распределяются один за другим и мы можем получать только по одному экземпляру на каждую строку кэша. Это значит, что для обработки всей колонии муравьёв нужно обратиться к основной памяти 100 раз, и что из каждой полученной строки кэша (64 байта) мы используем только 1 байт. Другими словами, мы отбрасываем 98% полученных данных. Это довольно неэффективный способ пересчёта муравьёв.

Ускоряем работу


Можем ли мы переписать нашу программу так, чтобы запросы к памяти были более рациональными, и таким образом повысить производительность программы? Да, можем.

Мы используем максимально наивный Data Oriented Design. Вместо моделирования муравьёв по отдельности мы смоделируем целую колонию за раз:

class AntColony {    public int size = 0;    public String[] names = new String[100];    public String[] colors = new String[100];    public int[] ages = new int[100];    public boolean[] warriors = new boolean[100];    // I am aware of the fact that this array could be removed    // by splitting the colony in two (warriors, non warriors),    // but that is not the point of this story.    //     // Yes, you can also sort it and enjoy in an additional     // speedup due to branch predictions.}AntColony antColony_do = new AntColony();// fill the colony with ants and update size counter// count the warrior antslong numOfWarriors = 0;for (int i = 0; i < antColony_do.size; i++) {    boolean isWarrior = antColony_do.warriors[i];    if (isWarrior) {        numOfWarriors++;    }}

Эти два примера алгоритмически эквивалентны (O(n)), но ориентированное на данные решение превосходит по производительности объектно-ориентированное. Почему?

Потому что ориентированный на данные пример получает меньше данных, и эти данные запрашиваются соседними блоками мы получаем 64 флагов воинов за раз и не тратим ресурсы впустую. Так как мы получаем только нужные нам данные, нам достаточно обратиться к памяти всего два раза, а не по разу для каждого муравья (100 раз). Это намного более эффективный способ подсчёта муравьёв-воинов.

Я выполнил бенчмарки производительности при помощи тулкита Java Microbenchmark Harness (JMH), их результаты показаны в таблице ниже (измерения выполнялись на Intel i7-7700HQ с частотой 3,80 ГГц). Чтобы не загромождать таблицу, я не указал доверительные интервалы, но вы можете выполнить собственные бенчмарки, скачав и запустив код бенчмарка.

Задача (размер колонии) ООП DOD Ускорение
countWarriors (100) 10 874 045 операций/с 19 314 177 операций/с 78%
countWarriors (1000) 1 147 493 операций/с 1 842 812 операций/с 61%
countWarriors (10000) 102 630 операций/с 185 486 операций/с 81%

Сменив точку зрения на решение задачи, нам удалось добиться огромного ускорения. Стоит также учесть, что в нашем примере класс муравьёв довольно мал, поэтому данные с большой вероятностью останутся в одном из кэшей процессора и разница между двумя вариантами не так заметна (согласно таблице задержек, доступ к памяти из кэша в 30-100 раз быстрее).

Даже несмотря на то, что представленные выше примеры написаны в свободном стиле всё является публичным, ориентированное на данные проектирование не запрещает использовать корпоративный подход со строгим контролем. Вы можете делать любые поля private и final, создавать геттеры и сеттеры, реализовывать интерфейсы, если вам так нравится.

Кто знает, может, вы даже поймёте, что все эти энтерпрайзные фишки необязательны? Но самое важное здесь в том, что надо перестать думать о муравьях по отдельности, а воспринимать их как группу. В конце концов, вы когда-нибудь видели муравья-одиночку?

Где есть один, будет несколько.

Майк Эктон

Но постойте! Почему ООП настолько популярно, если имеет такую низкую производительность?

  1. Нагрузка часто зависит от ввода-вывода (по крайней мере, в бэкенде серверов), который примерно в 1000 раз медленнее доступа к памяти. Если вы записываете много данных на жёсткий диск, то улучшения, внесённые в структуру памяти, могут и почти не повлиять на показатели.
  2. Требования к производительности большинства корпоративного ПО чудовищно низки, и с ними справится любой старый код. Это ещё называют синдромом клиент за это не заплатит.
  3. Идеи в нашей отрасли движутся медленно, и сектанты ПО отказываются меняться. Всего 20 лет назад задержки памяти не были особой проблемой, и best practices пока не догнали изменения в оборудовании.
  4. Большинство языков программирования поддерживает такой стиль программирования, а концепцию объектов легко понять.
  5. Ориентированный на данные способ программирования тоже обладает собственным множеством проблем.

Проблемы: допустим, нам нужно найти, сколько в колонии красных муравьёв возрастом старше 1 года. В объектно-ориентированном подходе эта задача решается довольно просто:

long numOfChosenAnts = 0;for (Ant ant : antColony) {    if (ant.age > 1 && "red".equals(ant.color)) {        numOfChosenAnts++;     }}

При ориентированном на данные подходе всё становится чуть более сложным, ведь нам нужно быть аккуратными с тем, как происходит итерация по массивам (нужно использовать элемент, расположенный по тому же индексу):

long numOfChosenAnts = 0;for (int i = 0; i < antColony.size; i++) {    int age = antColony.ages[i];    String color = antColony.colors[i];    if (age > 1 && "red".equals(color)) {        numOfChosenAnts++;    }}

А теперь представьте, что кому-то нужно отсортировать всех муравьёв в колонии на основании их имени, а затем что-то сделать с отсортированными данными (например, посчитать всех красных муравьёв из первых 10% отсортированных данных. У муравьёв могут быть странные правила, не судите их строго). При объектно-ориентированном решении мы можем просто использовать функцию сортировки из стандартной библиотеки. При ориентированном на данные способе придётся сортировать массив имён, но в то же самое время сортировать все остальные массивы на основании того, как перемещаются индексы массива имён (мы предполагаем, что нам важно, какие цвет, возраст и флаг воина связаны с именем муравья). [Также можно скопировать массив имён, отсортировать их и найти соответствующее имя в исходном неотсортированном массиве имён, чтобы получить индекс соответствующего элемента. Получив индекс элемента в массиве, можно делать с ним что угодно, но подобные операции поиска выполнять кропотливо. Кроме того, если массивы большие, то такое решение будет довольно медленным. Понимайте свои данные! Также выше не упомянута проблема вставки или удаления элементов в середине массива. При добавлении или удалении элемента из середины массива обычно требуется копировать весь изменённый массив в новое место в памяти. Копирование данных медленный процесс, и если не быть внимательным при копировании данных, может закончиться память. Если порядок элементов в массивах не важен, можно также заменить удалённый элемент последним элементом массива и уменьшить внутренний счётчик, учитывающий количество активных элементов в группе. При переборе таких элементов в этой ситуации мы, по сути, будем перебирать только активную часть группы. Связанный список не является разумным решением этой задачи, потому что данные не расположены в соседних фрагментах, из-за чего перебор оказывается очень медленным (плохое использование кэша).]

Именно подобные задачи нужно решать при помощи особенностей языков программирования, потому что написание таких специализированных функций сортировки очень монотонная работа. Что если язык программирования предоставит нам структуру, ведущую себя как массив структур, но внутренне ведущую себя как структура массивов? Мы смогли бы программировать в обычном объектно-ориентированном стиле, удобном для людей, и при этом обеспечивать высокую производительность благодаря оптимальному использованию оборудования.

Пока единственным языком программирования с поддержкой подобных безумных преобразований данных является JAI, но, к сожалению, он пока находится в состоянии закрытого бета-тестирования и недоступен широкой публике. Стоит поразмыслить, почему такая функциональность не встроена в наш современные языки программирования.

Best practices


Если вы когда-нибудь работали в энтерпрайзе и засовывали нос в его кодовую базу, то, вероятнее всего, видели огромную кучу классов с множественными полями и интерфейсами. Большинство ПО по-прежнему пишут подобным образом, потому что из-за влияния прошлого в таком стиле программирования достаточно легко разобраться. Кроме того, те, кто работает с большими кодовыми базами естественным образом тяготеют к знакомому стилю, который видят каждый день. [См. также On navigating a large codebase]

Для смены концепций подхода к решению задач требуется больше усилий, чем для подражания: Используй const, и компилятор всё оптимизирует! Никто не знает, что сделает компилятор, и никто не проверяет, что он сделал.

Компилятор это инструмент, а не волшебная палочка!

Майк Эктон

Я написал эту статью потому, что многие программисты считают, что нужно быть гением или знать тайные компиляторные трюки, чтобы писать высокопроизводительный код. Чаще всего достаточно простых рассуждений о данных, движущихся в вашей системе. [Это не значит, что код всегда будет прост и лёгок для понимания код эффективного умножения матриц довольно запутан.]. В сложной программе сделать это не всегда просто, но и этому тоже должен узнать каждый, если хочет потратить время на изучение подобных концепций.

Если вы хотите больше узнать об этой теме, то прочитайте книгу Data-Oriented Design и остальные ссылки, которые приведены в статье в квадратных скобках.

[БОНУС] Статья, описывающая проблемы объектно-ориентированного программирования:
Data-Oriented Design (Or Why You Might Be Shooting Yourself in The Foot With OOP).
Подробнее..

Что нужно знать перед началом роботизации процессов?

10.04.2021 12:12:31 | Автор: admin

Robotic Process Automation, или сокращенно RPA, набирает все больше оборотов на рынке СНГ и Казахстана, в частности. Участники рынка обеспокоились непрерывностью своей деятельности, эффективностью и, конечно, экономией. Пандемия указала нам на дыры, которые имеются в процессах, на то, как это опасно подвязывать процессы на людях и что полная диджитализация это не будущее, это сейчас.

За последние полгода я провела огромное количество встреч с клиентами, большими и не очень, компаниями и собрала наиболее часто встречающиеся вопросы чтобы написать эту статью. Этакий FAQ от владельцев бизнеса, руководителей и исполнителей, которые каким-либо образом хотят, начали или прошли роботизацию на своих участках.

Почему я могу об этом вещать? И кем выступает моя компания?

Мы телеком оператор, который решил, что имеет право и все шансы, чтобы стать лидером рынка по предоставлению услуг роботизации, так как успешно сделал это у себя и сэкономил на этом много сотен миллионов и рабочих часов. Мы решили не останавливаться только на себе и помочь другим компаниям исправить их процессы, стать эффективнее и гибче.

Для начала давайте поймем, что такое бизнес-процесс

Да да, нужно сначала понять именно это. Так как понимание процессов могут отличаться в умах заказчика и исполнителя, что далее приведет к спорам о том, что роботизация должна дать клиенту.

Если процесс начинается с формирования заявки на закуп в январе, а получаете акт выполненных работ от поставщика, который нужно занести в базу, в декабре этого же года, то это не 1 процесс.

Видимость начала и конца процесса должно быть в рамках 1 дня, даже 1 часа.

Процесс должен быть декомпозирован на несколько подпроцессов, процедур и функций, которые имеют свою логику, но в итоге ведут к достижению единой цели основного процесса.

Каждый такой подпроцесс требует отдельного внимания. Более того, можно начинать роботизировать только отдельные подпроцессы и постепенно налаживая весь процесс целиком. Бывает такое, что процесс начинается со сбора данных, где-то на месторождении, где водитель погрузчика пишет отработанные часы на листке бумаги, подписывает у главного по участку и далее относит этот документ в кадровую службу. Кадровик делает свой расчет и далее передает экономистам и в бухгалтерию. В результате множества таких документов формируется один реестр по количеству отработанных часов поставщика услуг, соответственно зарплаты, и доходная статья бюджета для перевыставления. Вроде бы процесс начался, когда водитель заполнял от руки бумагу, но для роботизации в формате AS IS, она начинается только когда данные попадут в цифровой формат.

Итого, процесс должен быть цифровым, рутинным и повторяющимся.

Сколько мне нужно роботов?

Этот вопрос очень тревожит всех, так как многие именитые вендоры выставляют счет за каждого робота в отдельности. Поэтому любой заказчик будет заинтересован в сокращении их количества.

Робот это, по сути, виртуальный сотрудник. У него в запасе 24 часа. Все что вы сможете уместить в 24 часа робот исполнит. Если процесс масштабный и в день вы совершаете таких операций 10 тысяч штук, где каждая операция занимает 1 минуту, то арифметика проста и ее поймет первоклассник. 10 тысяч штук делим на 1440 минут (столько минут в сутках) = 6.9.

То есть, для выполнения всех операции в рамках данного процесса, при полной загрузке, вам потребуется 7 роботов.

И так по каждому процессу.

Количество операций по процессу как 10 тысяч штук в день, это конечно очень много. И на нашем рынке может встретиться разве что в Ритейле.

Плюс ко всему, не стоит забывать, что зачастую робот работает с другими системами, соответственно, то, как они быстро будут отвечать на запросы также важно для скорости. Увы, не все зависит от робота.

Сколько будет стоить проект?

Стоимость проекта включает в себя несколько видов затрат: стоимость робота, то есть виртуального сотрудника, стоимость оркестратора, назовем его распределителем задач, либо менеджером роботов и траблшутером, студия (там где разработчик работает), виртуальные машины для каждого робота (он виртуальный, но все же сотрудник), Office, сервера либо облако.

Из опыта могу сказать, что основная статья расходов это конечно же сами роботы (более 75% чека).

Почему так дорого?

Многие клиенты почему-то ожидают что роботизация всего их отдела в 10 человек должна обойтись им примерно в 1000 баксов XD

Основные поставщики роботов приходят к нам из США. Где стоимость человекочасов достаточно высока. Один робот, который стоит примерно 8-10K USD, что ничтожно в сравнении с годовой заработной платой.

Конечно, для рынка, где средняя заработная плата 500 USD в месяц это может показаться крайне нерентабельно. Но, так ли это на самом деле? Стоит ли нам теперь отказывать себе в удовольствии на роботизацию? Нет. И вот почему.

Помимо ФОТ процесс может нести дополнительные расходы в виде штрафов, неисполнения обязательств перед третьей стороной, неправильные расчеты, ошибки и многое другое. Если считать эффективность с учетом всех этих факторов, то возможно вы уже вышли на точку безубыточности.

Также количество вендоров роботов из СНГ начинает расти. Может на сегодняшний день они немного уступают мировым аналогам, но справиться с процессами не сложными они могут и работают при этом стабильно.

Помимо этого, есть вариант облачного размещения роботов, аренды у большого интегратора, чтобы не нести дополнительные расходы.

Расчет эффективности не всегда имеет положительный знак для отдельно взятого процесса

Были и остаются кейсы, когда среди 10 процессов клиента, 1 или 2 могут показать отрицательный знак в расчете эффективности роботизации, но в общей совокупности всех процессов выходить в плюс. С чем это обычно связано? С тем, что происходит реинжиниринг процессов. Люди могут реально делать сверку документа быстрее чем человек если, к примеру нужно прочесть только 1 строку в документе. А для робота, ее еще нужно отсканировать и внести в папку чтобы он мог ее оттуда брать. Но, возвращаясь к пункту 4 вспомним, что количество таких быстрых операций человеком, это непременно ведет к ошибкам и процесс будет совершаться еще раз, а может не один. То есть, отрицателен он как правило при расчете с учетом только видимых расходов.

Ваш робот ломается, вы плохие ребята

Процесс работает в продуктовой среде, он уже приносит свои плоды, а владельцы бизнес-процессов могут пойти пить чай. Но не все так просто. В процессе роботизации есть несколько степов. Один из финальных степов это стабилизация. Это когда начинают всплывать исключения, которые не были учтены в начале роботизации данного процесса. Вы забыли, что из 1000 файлов в формате docx, у вас все-таки бывают иногда и GPEG-ы, но это о как редко. Все это требует проработки со стороны интегратора и относится к данной стадии нужно с пониманием. Это не робот сломался, это обязательная часть процесса роботизации бизнес-процессов.

Конфликт интересов и саботаж

В самом начале роботизации руководство должно понять: кто имеет в роботизации самый главный интерес, чего мы хотим достичь и как всех объединить. Также, нужно дать понять людям, что станет с ними после того, как часть их работы отдадут цифровому сотруднику. Уволят? Или перенаправят на другую позицию? Если диалог ранее не состоялся между сотрудниками, чья позиция уйдет под роботизацию, и теми, кто в ней заинтересован, то ждать саботажа не придется.

Саботаж может быть как на стороне роботизируемого отдела, так и на стороне IT. Имеет место быть синдром Not invented here, что означает избегание использования или покупки продуктов, исследований, стандартов или знаний из внешних источников, сильное предубеждение против идей извне.

Поэтому все стороны, которые будут каким-то образом вовлекаться в процесс роботизации должны собраться на берегу и обговорить весь план взаимодействия, интересов и ролей. Даже пилотного проекта.

Я не знаю с чего мне начать, как создать Roadmap?

Начинать надо с того, что даст самые быстрые результаты и что смотивирует команду, компанию и руководство пойти на роботизацию в целом.

В первую очередь это процессы простые в разработке, но имеющиеся большом количестве, то есть, повторяющиеся очень много раз и, как следствие, требующего большого вовлечения сотрудников. Начинать разработку со сложного процесса не желательно. Зачастую это может потребовать полгода разработки, а экономия при этом окажется незначительной.

Сложные процессы это уже следующий уровень, когда вы хорошо знакомы с RPA, нарастили экспертизу и готовы расширяться.

При неполучении быстрых результатов падает мотивация и продолжить затею вряд ли получится, как минимум, вам не выделят бюджет под инициативу.

Подытожим, в первую очередь работаем над простыми процессами в большом количестве, только затем сложные в малом количестве.

Я найму свою команду, и мы будем делать сами

Каждая компания, на самом то деле, может делать роботизацию самостоятельно. Вопрос только в том, насколько она сможет расширяться и продолжать поддерживать сложные операционные процессы, которые затрагивают целые департаменты.

Компании, которые не содержат штат IT специалистов, у которых есть достаточно времени чтобы уделять его бизнес заказчикам, делать доработки и постоянно обучаться, возможно, но нелегко и долго.

Каждый должен заниматься тем, что у него хорошо получается, прямо как у людей. Если вы транспортная компания, отдайте лучше роботизацию на аутсорс, а сами наслаждайтесь выигранным временем и расширяйтесь.

Я как консультант, почти все решения рассматриваю на цифрах. Если ваша компания не имеет продуктовой культуры, вы не руководили разработчиками, но горите желанием делать роботизацию самостоятельно, просто посчитайте сколько это будет стоить. Помимо найма и обучения сотрудников, учитывайте альтернативные издержки и потери от неэффективных процессов.

Внедрение роботов облегчает нашу с вами работу, и экономическая выгода от нее не заставит себя ждать. Главное выбрать правильного поставщика услуг и вендора.

Подробнее..

Трансформация контакт-центра в платформу Customer eXperience в погоне за клиентским счастьем

31.03.2021 22:18:24 | Автор: admin
Елена Смирнова, ведущий менеджер по продуктовому маркетингу CTI, написала статью о клиентском опыте (Customer eXperience, CX) для издания CRN. С разрешения издания, публикуем здесь полную версию этого материала.
Увлечение темой клиентского опыта связано не с модой на новые слова и концепции это насущная необходимость перестраивать бизнес под изменяющееся поведение и ценности клиентов. Те организации, которые поймут это быстрее других, и главное начнут системную трансформацию своих бизнес-процессов в связке с внедрением инновационных технологий и станут победителями в конкурентной борьбе.

CX это сохранение и последующее эффективное использование всех взаимодействий клиента c бизнесом. В таких историях ценно все: рациональные, эмоциональные, физические и даже духовные аспекты контакта, например, предпочтения клиента, частота покупки, величина среднего чека, реакция на рекламные акции, вежливость диалога с сотрудниками контактного центра и т.д.

Исследователи потребительского поведения продолжают фиксировать тренд смещения фокуса в сторону клиентского опыта. Людям мало просто купить какую-то вещь или получить услугу, становится важно проактивное понимание предпочтений, быстрое реагирование на запросы, безапелляционное решение всех вопросов. Так, например, результаты исследования, проведенного в 2020 году компанией SAP Customer Experience CIS в 7 крупных отраслях показало, что более 50% клиентов, испытавших негативный опыт, заявили, что сократили расходы или полностью перестали пользоваться услугами компании. Данные из отчета McKinsey 2020 говорят о том, что в 5,7 раз больше доходов у компаний с превосходным клиентским опытом, чем у конкурентов. При наличии малейшего негатива, вероятность того, что покупатель уйдет к конкуренту в 4 раза выше, даже если качество самого продукта ему нравится.

Вывод прост: клиенты хотят испытывать положительные эмоции от процесса взаимодействия с компанией, и этот положительный опыт все чаще становится ключевым моментом при выборе продукции или услуги. Этот факт меняет парадигму бизнеса по обслуживанию клиентов: компании из поставщиков превращаются в провайдеров позитивного обращения, отношения и сервиса. Каждая точка контакта с клиентом становится местом создания дополнительной ценности, оценки бренда и опыта взаимодействия с бизнесом. Причем понятие клиент рассматривается максимально широко это и внешний клиент (потребитель услуг или товаров), и внутренний клиент (сотрудник, партнер или подрядчик).

Возникают вопросы: как обеспечить клиентский (в самом широком смысле) опыт на высшем уровне? И как организовать процессы в бизнесе, чтобы реализовать CX в конкурентное преимущество?

Итак, обратимся к самому процессу обслуживания покупателей и потребителей услуг. Основными точками входа клиента в организацию являются офисы продаж или торговые залы, куда они приходят лично, интернет-ресурсы в виде корпоративных сайтов, онлайн-магазинов, профилей в социальных сетях и, конечно же, контакт-центр, который принимает текстовые и голосовые обращения и является незаменимым звеном в цепочке управления клиентским опытом.

Требования рынка к современным контакт-центрам высоки, эксперты системного интегратора CTI лидера индустрии клиентского обслуживания в России выделяют следующие основные технологические аспекты, которые позволяют оправдать столь высокие ожидания современных клиентов:

Диалоговые боты на основе искусственного интеллекта обеспечивают доступность услуг в режиме 24х7, при этом они значительно снижают нагрузку на операторов, решая все стандартные вопросы клиентов.
Омниканальность позволяет клиенту обращаться в компанию из любого канала коммуникаций, при этом сохраняется и доступна оператору вся история его предыдущих обращений.
Голосовая биометрия позволяет сделать процесс идентификации клиента в дистанционных каналах обслуживания безопасным, удобный и быстрым.
Инструменты речевой аналитики решают ключевой вопрос контроля и повышения качества обслуживания.
Автоматизация исходящего обзвона повышает эффективность маркетинговых кампаний.
Автоматизация разработки скриптов упрощает процесс создания сценариев обслуживания и диалогов.
Оптимизация графиков работы персонала (WorkForce Management, WFM) обеспечивает непрерывность процесса обслуживания и сокращение времени ожидания ответа с одной стороны, и продуктивную работу сотрудников контакт-центра с другой стороны.
Роботизация (Robotic Process Automation, RPA) всей рутинной работы позволяет оператору сосредоточится на процессе обслуживания клиента и ускорить выполнение задачи.
Инструменты аналитики рабочего стола (Desktop & Process Analytics, DPA) записывают и анализируют все действия оператора при выполнении задачи и позволяют сформировать оптимальные алгоритмы.
Интеллектуальные базы знаний и подсказки облегчают работу операторам и помогают быстро решать вопросы клиента.
Аналитические CRM с системой принятия решений обеспечивают персонифицированные продажи, предоставляя оператору информацию о клиенте и его предпочтениях и подсказывая что именно нужно ему предложить

Рассмотрим подробнее некоторые из перечисленных технологических трендов.

Тема диалоговых роботов на основе искусственного интеллекта сейчас является хайповой и мы видим множество решений на рынке, обещающих мгновенный успех.
Область применения диалоговых роботов сейчас очень обширна:
первый контакт с клиентом для классификации цели его обращения;
текстовые боты на сайтах, в социальных сетях и в мессенджерах;
перевод обращения на сотрудника с нужными навыками и квалификацией;
предоставление информации о продуктах без участия оператора контакт-центра;
welcome-контакт с новым клиентом, где робот может подсказать с чего начать;
оформление заявок и документов;
автоматизация работы HR;
идентификация клиента, извлечение из систем банка информации и предоставление клиенту в автоматизированном режиме без участия оператора;
телемаркетинговые опросы;
коллекторская работа с должниками
и многое другое.

При реализации подобных сервисов зачастую можно получить взрывной эффект, который становится приятным сюрпризом для заказчиков. Несколько примеров из нашей практики по внедрению систем самообслуживания на базе диалоговых роботов, показывающих насколько эффективна такая автоматизация:

На одном из проектов через один месяц работы системы в продуктивном режиме почти 50% вопросов в клиентской службе стали решаться без участия человека, так как большую часть запросов можно описать в алгоритме и доверить их обработку роботу.
В некоторых сценариях коэффициент автоматизации достигает 90%, поскольку постоянно повторяющиеся задачи, например, по предоставлению справочной информации решаются роботами, а операторы не тратят на это время и могут обрабатывать более сложные запросы клиентов.
Уже эффективно работают достаточно сложные сценарии, где глубина диалога человека с роботом может достигать 34 шага, что позволяет максимально точно определить зону интереса клиента и обслужить его в автоматическом режиме.

Но, есть заблуждения, благодаря которым на технологии диалоговых ботов возлагаются завышенные ожидания. Многие считают, что современные роботы устроены так, что в них можно загрузить записанные фразы и нейронная сеть сделает правильные выводы, а на выходе получится диалог с клиентом, который нельзя будет отличить от разговора с реальным человеком. На самом деле это не так, и во всех проектах внедрения пока требуется существенное воздействие со стороны экспертов, от компетентности которых в основном и зависит, будет ли приятно общаться с этим роботом, или же общение с ним вызовет стойкое желание переключиться на оператора.

Омниканальность следующий тренд с развитием дистанционного обслуживания все чаще становится необходимым условием. Клиенты стали очень требовательными, они хотят использовать тот канал коммуникаций, по которому удобно общаться именно сейчас. Они хотят, чтобы их вопрос решался быстро и не нужно было повторять его несколько раз. Для этого используются системы, которые позволяют сотруднику контакт-центра обрабатывать обращения, поступающие из различных каналов коммуникаций, будь то телефон, мессенджеры, социальные сети, чат на сайте или электронная почта.

Одним из таких решений является омниканальная платформа CTI Omni, разработанная системным интегратором CTI. Она позволяет оператору контакт-центра в едином окне обрабатывать обращения, поступающие из нескольких каналов одновременно, что существенно повышает производительность их работы. Оператор может мгновенно идентифицировать клиента и понять контекст его предыдущих обращений благодаря единой истории и встроенному профилю клиента. CTI Omni содержит алгоритмы интеллектуальной маршрутизации, которые, определяя тематику обращения по ключевым словам, переадресуют его на оператора с нужными навыками и компетенциями. Исторические и online отчеты позволяют контролировать качество обработки каждого обращения. В решение встроен универсальный механизм интеграции с любыми чат-ботами и голосовыми платформами.

Результаты внедрения платформы CTI Omni показывают рост индекса потребительской лояльности в 2 раза за счет удобства общения с компанией и быстрого решения вопросов скорость обслуживания повышается в среднем на 20%. Кроме того, сокращение операционных расходов составляет около 25%. Это происходит за счет использования более дешевых цифровых каналов коммуникаций, автоматизации работы операторов и оптимизации их численности в контакт-центрах.

Технология голосовой биометрии существует на рынке уже более 10 лет. Однако, если говорить о биометрии в клиентском обслуживании, число внедрений в России пока невелико. Основными стоп-факторами для распространения до недавнего времени было отсутствие правового регулирования применения биометрических технологий, недоверие к их безопасности и надежности, а также сомнения заказчиков в том, что немалые инвестиции во внедрение высокотехнологичного продукта будут оправданы. Тем не менее, ситуация существенно изменилась после запуска единой биометрической системы, для нормативно-правового обеспечения которой был принят Федеральный закон 482-ФЗ О внесении изменений в отдельные законодательные акты Российской Федерации. Вслед за этим, ключевые игроки банковского рынка, таких как Сбербанк, ВТБ, Альфа-банк внедрили у себя биометрическую аутентификацию. Более того, некоторые компании опубликовали данные о возврате инвестиций после внедрения. Так ВТБ заявил о том, что внедрение системы идентификации клиентов по голосу позволило сократить количество схем, используемых мошенниками, и спасти 26 миллионов рублей всего лишь за 1 год работы.

Технология голосовой биометрии переводит процесс идентификации на новый уровень. Как утверждают специалисты Ростелекома, вероятность ошибки 1 на 10 000 000, при этом вычислить мошенников можно как в реальном времени, так и в исторической перспективе. Причем для выявления совершенно не обязательно, чтобы слепок голоса злоумышленника уже попал в базу. Важно отметить, что применение биометрических технологий кроме предотвращения прямых потерь, существенно снижает репутационные риски компании.

На распространение технологии голосовой биометрии в 2020 году повлиял и факт массового перехода на дистанционное обслуживание в сегменте B2C. Благодаря тому, что биометрическая идентификация личности имеет очень высокую точность, компании получили возможность предоставлять в дистанционных каналах обслуживания большее количество услуг. Это важно не только в условиях эпидемии, когда многие клиенты предпочитают онлайн-консультации, но и в регионах с невысокой плотностью офисов, там, где нужно тратить много времени на личный визит. Такие варианты использования биометрии нацелены на повышение лояльности клиентов. Также положительно влияет на лояльность то, что процесс установления личности с использованием биометрии значительно (в среднем, в 4 раза) быстрее традиционного, он не требует пакета документов и запоминания кодовых слов. У технологии биометрии есть большой потенциал. Законодательная база постоянно дорабатывается, расширяя возможности применения данных технологий. Это позволит банкам вводить в промышленную эксплуатацию оплату по лицу и голосу в магазинах и кафе.

Следующим трендом является речевая аналитика, которая получает широкое распространение как для контроля качества обслуживания, так для выявления настроений клиентов. В последнее входит и реакция, например, на запуск новых продуктов и услуг, и выявление намерений клиента уйти в конкурирующую организацию. По данным национальной академии контакт-центров, более 60% компаний используют возможности речевой аналитики для сегментации клиентской базы и более точного таргетирования продуктовых предложений Кроме того, анализ диалогов позволяет определять достаточен ли уровень подготовки сотрудников, которые общаются с клиентами, правильно ли они отрабатывают согласованные скрипты, как отвечают и реагируют на запросы. Ведущие поставщики платформ речевой аналитики (ZOOM, Verint, ЦРТ SmartLogger и другие) предоставляют данные практически в режиме реального времени. Это позволяет оперативно реагировать на спорные или нерешенные вопросы клиентов, корректировать бизнес-процессы, например, при запуске новых продуктов. Так, например, коробочное решение ZOOM Speech Analytics (Eleveo) имеет быстрый и высокоточный речевой движок для обработки аудио-звонков и записей экранов с минимальным использованием оборудования. С помощью этого движка можно анализировать содержание разговоров операторов, управлять запросами поиска по обращениям клиентов, выделять полезную информацию, например, случаи эмоционального недовольства, присутствие длинных пауз, а также наличие нерешенных претензий.


От автоматизации контакт-цента к платформе CX время пришло

В конфигурации, соответствующей всем технологическим трендам, контакт-центр становится реальным центром генерации прибыли. Он позволяет принимать обращения клиентов из голосовых и цифровых каналов, эффективно обрабатывать входящий поток, контролируя все этапы процесса обслуживания, вести единую историю взаимодействия с каждым клиентом, выявлять проблемы, ошибки и отклонения в действиях сотрудников и причины их возникновения. Но, к сожалению, в реальности у многих организаций ситуация выглядит иначе. Самая серьезная проблема заключается в отсутствии связи процессов обслуживания в контакт-центре с точками продаж и внутренними подразделениями компании, отсутствии единой товароучетной системы для онлайн и офлайн-точек, единой клиентской базы и бонусной системы, сложностях в анализе информации, которая фиксируется в разных несвязанных системах.

Из опыта реализованных проектов эксперты CTI отмечают, что в последнее время многие компании ориентируются на создание полноценной платформы Customer Experience, в которой предусмотрена прозрачная интеграция основных частей организации: КЦ офис продаж бэк-офис. Эта связка, усиленная технологическими решениями, обеспечивает получение заказчиком бизнес-результатов на всех этапах клиентского обслуживания. Такой подход позволяет извлекать из уже используемых технологий максимальную пользу.

В качестве примера можно привести уже рассмотренную выше технологию речевой аналитики. В контакт-центре она обеспечит контроль качества обслуживания клиентов, позволит отследить как с ними общаются операторы, определить эффективность маркетинговых акций. И ту же технологию можно использовать в офисах продаж для контроля работы сотрудников, анализируя как происходит продажа, по какому скрипту общается продавец, выявлять наиболее эффективный скрипт, реагировать на отклонения и внештатные ситуации. Таким образом, инвестируя в приобретение одной технологии бизнес получает сразу много выгод.

Единое омниканальное окно для приема обращений из наиболее востребованных у клиентов каналов: телефонная связь, электронная почта, мобильные приложения, SMS, мессенджеры, соцсети, веб-чаты, онлайн-формы или звонки с сайта позволяет вести общий реестр обращений и единую история взаимодействия с клиентами. Можно отслеживать путь клиента (Customer Journey Map) и выявлять проблемные зоны и точки роста, быстро реагировать на запросы, формировать персонифицированные предложения.

Управление клиентским опытом должно выстраиваться на уровне корпоративной стратегии при помощи инновационных технологий, способствующих совершенствованию и оптимизации бизнес-процессов. Глобально, основными инструментами тут являются аналитика больших данных, облачные вычисления и конечно, искусственный интеллект. С точки зрения клиентского обслуживания, эти технологии обеспечивают персонализацию торгового предложения, оптимизацию продаж, управление каналами продаж. Необходимо собирать актуальные данные, чтобы лучше понимать желания и намерения клиентов, обрабатывать эти данные в режиме реального времени и обеспечивать необходимый уровень персонализации взаимодействия в нужный момент.

С точки зрения взаимоотношений организация-сотрудники, тренд на CX приводит к трансформации офисного пространства, многие компании уже взяли курс на создание новой среды для эффективного и комфортного взаимодействия, которая позволяет сотрудникам продуктивно работать как внутри офиса, так и находясь за его пределами.
Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru