Скучное вступление


Не так давно, мне довелось участвовать в разработке некоего программно-аппаратного комплекса для одной американской компании. Разрабатывал я бэкенд, немного фронтенд, сращивал устройства с облаком (IoT то бишь). Стек технологий был обозначен четко. Ни в право, ни в лево — enterprise, одним словом. В определенный момент меня перекинули в помощь на фронтенд POS (Point of Sale) веб приложения.

Проблема. Становится интересней


Всё бы ничего, но веб приложение разрабатывалось для работы в 6 тыс. офисах по всей территории Америки (для начала). Где, как оказалось, с интернетом могут быть проблемы. Да да, в той самой, продвинутой Америке! Проблемы с покрытием не только проводного интернета, но и мобильной связью! Т.е. плохой интернет канал (часто, мобильный) — вполне себе обычная история для небольших американских городов.

А это же POS… Тут, понимаешь, клиенты стоят, надо инвойс быстро распечатать… Тормозов быть не должно! И livesearch… Были обсуждения, прикидки, в итоге — не стали грузить бэкенд запросами (трафик, опять же). Сошлись на том, что веб приложение должно по-максимуму подгружать данные и делать, тот же поиск, локально. Речь идет, конечно, о данных, размер которых позволяет это сделать.

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

Кэш


Первое, что было сразу сделано (и это логично) — кэширование и хранение получаемых данных локально. По возможности — долго, насколько позволяет секьюрити. Для каких-то данных лучше использовать localStorage, для других — sessionStorage, а что не помещается — можно просто хранить в памяти. Мы использовали ангуляр, поэтому нам вполне подошел для этих целей angular-cache.

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



Сделали, конечно, инвалидацию кэша и тп. Трафик сократился, но отклик при первоначальном обращении к страницам оставался непозволительно большим.

Фоновая подгрузка ресурсов


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

$q.all([
  Service1.preLoad(),
  Service2.preLoad(),
  …
  ServiceN.preLoad()
]);

Где Service.preLoad() — функция, возвращающая promise ресурса страницы.

Но есть проблема — в этом случае все промисы выполняются одновременно, т.е. одновременно тянутся все 100500 ресурсов. А канал-то — не резиновый! Плюс у нас мега-запрос к стороннему сервису, который долго и медленно качал много данных. В итоге все параллельные запросы выполнялись по времени почти столько же, сколько и этот мега-запрос.

Окей, загрузим по-порядку:

Service1.preLoad()
  .then().Service2.preLoad()
  …
  .then().ServiceN.preLoad();

Оно, воде как, стало лучше, но не очень — если пользователь сразу шел в “неправильный” раздел — будем ждать прогрузки очереди, пока не доберемся до ресурсов этого раздела. В общем, тут бесконечная эвристика. Что-то лучше пачкой загружать, что-то отдельно в параллели, и тд и тп… Хотелось, однако, какого-то более системного подхода.

Очередь


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



Так то оно так, только есть еще одно, не резиновое место — размер кэша.

Система ресурсов


Чем глубже погружался в эту проблему, тем сильнее возникало дежавю — я это уже проходил! Ограниченные ресурсы памяти, фоновая загрузка, выгрузка, приоритеты… это же… это же — типичная система ресурсов игрового движка! Едешь на машинке — локации подгружаются, что далеко позади — выгружаются. Еще термин специальный был для игровых движков — поддержка стримминга… Лет 5 жизни я провел в геймдеве, это было круто…

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



Проблема веб приложения, что и в геймдеве — предсказать где будет пользователь, чтобы загрузить ресурсы заранее. Решение №1 — дизайн, конечно. Направить пользователя по предсказуемому (а иногда, и единственному) маршруту. Что не удается решить дизайном — статистика + эвристика.

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

Вариантов реализации — масса. Самый предсказуемый — предпроцессинг, ручной либо автоматический. Мы должны указать, какие ресурсы в локации/разделе нам будут нужны, а каким-то снизить приоритет.

LOD


И тут Остапа понесло… В смысле, в голову полезли геймдев-трюки, применимые в веб разработке. Один из них — LOD (Level of Detail), уровень детализации. В геймдеве он применяется для текстур и моделей. Можно сразу прогрузить мир с минимальным уровнем детализации, а стриммить уже детализированные модели текстуры. И игрок всегда что-то видит, и даже, может играть.

Т.е. нужна система LOD для загружаемых данных! Для веб подходит самый примитивный вариант — два уровня детализации. Сначала грузим начальные данные, которые видит пользователь (первые страницы таблиц, например).



Данных получается мало, грузятся быстро. А бэкграундом… бэкграундом грузятся уже LOD’ы “по-тяжелее”.

Компрессия


Впихнуть невпихуемое — почти стандартная задача игродела. Ну так давайте раздвинем границы localStorage! Берем какой-нибудь LZ-компрессор и вперед! Да, но localStorage может хранить только строки… Ну, тогда сгодится, например, lz-string.js. Компрессия уже не та, но даже -20%, когда в распоряжении всего 5Мб — это совсем не плохо! И как бонус — секьюрные дела, в localStorage будет не открытый текст, а китайские знаки.

А дальше-то, что?


Дальше… дальше мысль несется к неизведанным глубинам. В памяти всплывает VFS (Virtual File System) — прослойка между ресурсной системой игры и файловой системой операционной системы. Обычно, всё крутится вокруг data-файла, к которому можно обратится как к файловой системе. Прочитать файл, записать… А что если сделать VRC (Virtual REST Call)!? Тогда ведь можно поддержать работу веб приложения вообще при отсутствии интернет соединения! В какай-то степени, конечно, но всё же.

Контроллеры общаются с менеджером ресурсов. Он что может отдать — отдает сразу, все остальные запросы отдаются VRC. А он, в свою очередь, уже самостоятельно синхронизирует своё состояние с бэкендом и, по мере загрузки, информирует об этом.



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

Но статья конкретно не про это. А про то, как порой неожиданно, всплывает опыт давно минувших дел…

Приятного кодирования, друзья!
Поделиться с друзьями
-->

Комментарии (13)


  1. Ashur_451
    06.06.2016 15:03
    +1

    Немного напомнило мне как в Learning How To Learn на курсере рассказывали про свойства нейронных паттернов. Один нейронный паттерн созданный при обучении одной вещи может быть использован частично или полностью в выполнении другой. Я на пример, всегда видел много аналогий в музыкальной теории и математике.

    PS: Ссылка на курс, очень советую, ru.coursera.org/learn/learning-how-to-learn


  1. Zav
    07.06.2016 05:41
    +1

    Оригинальное управление ресурсами — возьму на вооружение, спасибо.

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

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

    Уважаемый модератор — если приведенные ссылки «не приемлимы» — просто удалите их, не надо мне в очередной раз блокировать аккаунт, пожалуйста.


    1. ilya_radinsky
      07.06.2016 07:07

      Согласен. Собственно, вся эта возня с кэшами возникла для исключения потенциального ддос'а — livesearch.


  1. amakhrov
    07.06.2016 06:21

    > А что если сделать VRC (Virtual REST Call)!? Тогда ведь можно поддержать работу веб приложения вообще при отсутствии интернет соединения!…

    Вы только что описали работу ServiceWorker :) developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API


    1. ilya_radinsky
      07.06.2016 07:17

      Очень похоже, до неприличия ;) Не знал, спасибо.


  1. Proteck
    07.06.2016 12:46

    Было бы интересно увидеть результаты, какое ускорение получилось по каждому из улучшений


    1. ilya_radinsky
      07.06.2016 13:43

      Честно говоря, таких цифр нет — просто боролись за приемлемую скорость загрузки и уменьшение трафика.


  1. atc
    07.06.2016 12:56

    Очень похоже на клиентские data layer пакеты, вроде ember-data, orbit.js, fortune.js, особенно в части построения графа зависимостей, оптимизации запросов, кешировании.
    Автор, вы их не рассматривали?


    1. ilya_radinsky
      07.06.2016 13:45

      Нет, не рассматривали. У нас были жесткие рамки: бэкенд — MS (ASP.Net, MS SQL, Azure и тп), фронтенд — ангуляр (с поддержкой планшетов и мобильников).


      1. atc
        07.06.2016 13:49

        С ангуляром эти библиотеки вполне совместимы, к бекенду так же требований практически не предъявляют за исключением протокола (чаще всего используется jsonapi.org), но и это вполне преодолимо, так как в большинстве data layer возможно написание адаптеров под свое api.


        1. ilya_radinsky
          07.06.2016 14:12

          Деталей особо не помню, но мы в свое время ничего подходящего для себя не нашли. Те же веб сокеты мы смогли использовать только в рамках SignalR и Azure IoT Hub.


  1. yokotoka
    07.06.2016 13:46

    Кстати, насчёт оффлайновой работы POS-терминалов есть ещё вот такая интересная штука:
    https://www.youtube.com/watch?v=uyZKWyciSXY

    https://github.com/gritzko/swarm


  1. asvechkar
    09.06.2016 12:34

    Для больших объемов можно использовать IndexedDB. В одном из проектов мы загружали туда часть КЛАДР (~300т. записей). Нормально справляется. Причем кроме веб клиента сделали на Cordova обертку для мобильных устройств.