В первой части статьи был высказан тезис о том, что виной низкого быстродействия создаваемого нами CRM-приложения была SPA-архитектура. Для кого-то такое предположение могло показаться, мягко говоря, неожиданным и даже оскорбительным, учитывая стремительно растущую популярность данного подхода в разработке WEB-приложений, да и мы, как и многие современные разработчики, тоже вполне успешно осваиваем новые технологии, однако на примере данного проекта нам удалось эмпирическим путём нащупать ту грань, где стоит дважды подумать, прежде чем делать ставку на новое, и как раз об этих деталях и пойдёт речь во второй части статьи.
Выбирая SPA-архитектуру, мы ожидали получить следующие преимущества.
1. Высокий уровень интерактивности для пользователя — страница является практически полноценным клиентским приложением, снимая с пользователя бремя ожидания загрузки страниц и сглаживая неудобные глазу мерцания во время навигации.
2. Снижение издержек при обмене данными — вместо полноценных страниц с сервера загружаются только бизнес-данные, что для слабых или перегруженных каналов, например, для мобильного интернета.
3. С сервера снимается дополнительная нагрузка, связанная с подготовкой представления для клиента.
4. Высокая скорость разработки и сопровождения благодаря декларативному описанию шаблонов.
5. Отсутствие проблем в согласовании состояния клиента и сервера, так как состояние приложения во время его работы полностью сохраняется и управляется на клиенте.
6. Чёткое разделение обязанностей при разработке: на сервере реализуется только бизнес-логика и REST API для доступа к данным, а реализация клиента мало зависит от особенностей серверной бизнес-логики.
7. Данные возможно кэшировать без учёта представления, а на клиенте — даже модель целиком в Local Storage, что позволяет реализовать Offline-режим.
8. Возможность использования одного REST API для разных клиентских приложений.
При разработке мы использовали следующий стек технологий:
1) Microsoft ASP.NET MVC + WebApi на сервере под управлением IIS 8.
2) на клиенте — Angular JS версии 1.2.0 (последней на тот момент) и RequireJS.
Дальнейшее повествование будет основываться на специфике выбранных технологий.
В результате мы представляли себе следующую концептуальную схему.
От ASP.NET MVC требовалась генерация единственной «заглавной» страницы: один маршрут, один контроллер, одно действие и одно представление, не требующее мастер шаблона. Взаимодействие клиента с сервером осуществляется через REST API, который был реализован с помощью великолепной библиотеки WebApi версии 2.
Реализация клиента представляла собой набор статических файлов: CSS-стилей, HTML-шаблонов и JavaScript-скриптов, собираемых методом AMD. Для разработки мы использовали Microsoft Visual Studio (хотя в данном случае можно было бы взять и другой инструмент, например, WebStorm — это несомненный плюс, так как поддержка AngularJS у Visual Studio откровенно слабая).
Общая архитектура нашего SPA-приложения представлена на рисунке.
Приложения на AngularJS отлично работают с сервером в рамках концепции CRUD, однако несмотря на то, в нашей системе функции просмотра, создания и редактирования сущностей похожи на CRUD, на деле они обладают гораздо более сложной логикой, чем простое создание, изменение или удаление объектов в базе данных, и, например, создание задачи в CRM представляет собой довольно объёмный бизнес-процесс, включающий в себя некоторое количество проверок и ветвлений, и полученные в результате изменения модели данных обязательно касаются других объектов системы, что в конечном итоге должно отразиться на логике клиента, в результате чего после проведения операции создания задачи большую часть данных приходится заново загружать с сервера, чтобы актуализировать общее состояние системы.
Значит, создав задачу, нельзя обойтись только одним созданием соответствующего объекта в недрах клиента и добавлением его в коллекцию, а это нарушает концепцию того, что тонкий клиент должен быть тонким и не допускать размазывания серверной и клиентской логики — логика клиента, это не более чем логика UI, или user interaction. Таким образом, ожидаемое преимущество SPA-архитектуры о том, что состояние приложения во время его работы полностью сохраняется и управляется на клиенте, не проявило себя, а лишь создало дополнительные трудности реализации.
Тогда что является состоянием клиентского приложения, которое сохраняется во время жизненного цикла клиентского приложения? В основном это как раз состояние UI (выбранные пользователем параметры отображения, последние закладки, фильтры, последнее расположение элементов, удобное пользователю и т.п.) и данные, которые не подвержены частым изменениям в течение длительного времени, но активно использующиеся для правильной работы UI. Из самого основного, это — ACL, в зависимости от параметров которого будут показываться, скрываться или «отключаться» от пользователя некоторые элементы управления, история навигации, информация о текущем пользователе.
Второй момент заключается в том, что (кто работал с AngularJS, те знают) хоть в рамках приложения и можно получить URL любой глубины, чтобы, например, сохранить его в закладках и переходить по нему напрямую в нужный раздел, на самом деле каждая такая ссылка заставляет браузер загрузить всё приложение целиком, которое после загрузки должно быть сначала проинициализировано, что влечёт за собой необходимость получить с сервера все данные для работы, а затем выполнить переход «вглубь» по ссылке. Это чаще всего требует выполнения дополнительных запросы к серверу, и в итоге переход по ссылке может быть мучительно долгим, особенно в первый раз. Для нашей CRM обмен ссылками между сотрудниками, открытие контента в дополнительных вкладках —очень частая операция. Таким образом, выигрыш времени на малом объёме пересылаемых данных фактически нивелируется за счёт необходимости выполнения большого количества операций обмена данными с сервером перед тем, как клиентское приложение начнёт полноценно функционировать.
Теперь самое время предоставить больше информации о технических проблемах, с которыми мы столкнулись при реализации CRM, используя SPA-архитектуру и фреймворк AngularJS.
Утечки памяти и падение производительности
При тестировании пользователи стали замечать заметное падение производительности через некоторое время непрерывной эксплуатации. Мы провели серию синтетических тестов, учитывая усреднённый характер использования приложения и получили следующий пример профилирования работы приложения с самого начала старта, поочерёдно выполнив каждую из функций вперёд и в обратном направлении в течение 15 минут. Указано среднее значение использования памяти за это время.
Ничего страшного не наблюдается. Основной потребитель памяти: коллекции с данными. Причём более подробное профилирование на коротком промежутке времени показало, что память утилизируется нормально, GC справляется со своей задачей.
Но при больших интервалах времени начинает накапливаться буфер не очищаемой сборщиком мусора данных, проще говоря, утечка. В течение двух-трёх часов работы в обычном режиме объём потребляемой памяти вырастает с 200 до 600 Мб, а в течение рабочего дня до 1 Гб и больше, что недопустимо. В процессе отладки нашлись явные недоработки, связанные в основном с захватом ресурсов в замыкание — проблема известная. Львиная доля утечек приходилась на внешние чужие библиотеки, которые в совокупности с используемым кодом приводили к данной проблеме.
Затраченное на отладку время показало, что с утечками бороться можно, но для уже написанной и рабочей системы такого рода оптимизации сильно трудоёмки. Таким образом, необходимо больше внимания уделять этом в процессе разработки, проводя профилирование после каждого небольшого этапа работ, чтобы исключить накапливаемую утечку, но как раз это и уменьшает привлекательность выбранной архитектуры для разработчика, ищущего эффективный способ быстро решить задачу.
Так удалось ли нам оправдать ожидания, о которых я говорил в начале статьи? Лишь в какой-то мере. Это далеко не первый опыт использования AngularJS для разработки веб-приложений, но до разработки CRM нам не приходилось сталкиваться с указанными проблемами.
В результате, мы задались вопросом, есть ли способ взять лучшее из SPA-архитектуры, и совместить это с классическим подходом разработки приложений под веб? Чтобы ответить на этот вопрос, мы выбрали гибридный подход.
Гибридный подход
Мы решили разделить тяжёлые независимые части приложения на отдельные реальные страницы, каждая из которых представляет собой отдельное приложение. На первый взгляд выглядит как усложнение, но в итоге это не так.
Каждая страница, являясь приложением, содержит в себе только тот функционал, который используется в данной части (похоже на возврат к тому, что было раньше). Каждая страница не является пустой, а содержит минимальный HTML, который сразу показывается пользователю. Если некий контент открывается в приложении во всплывающем окне, то по ссылке этот контент уже открывается как отдельная страница, и открывается быстро. Это очень похоже на специальные страницы для краулера при поисковой оптимизации SPA-приложений.
Нам не хотелось отказываться от высокой интерактивности при навигации пользователя внутри приложения, но как выяснилось, опасения были напрасны. Если страницы с HTML контентом правильно кешируются (как на сервере, так и на клиенте), разница в скорости навигации не видна невооружённым глазом, и рядовой пользователь разницы не видит вообще. В остальном всё работает так же —сначала кэшированная страница быстро открывается, затем исполняется приложение, которое загружает бизнес-данные используя REST API и рендерит шаблоны точно так же, как это делает SPA-приложение.
Общее состояние приложения и пользователя сохраняется в Session Storage, это позволяют наши технические требования к браузеру. Для кеширования же практически ничего не поменялось, так как все страницы с точки зрения клиента являются статичными.
Единственное, что никак не победить —полноценный Offline режим, это исключительно прерогатива SPA.
Резюмируя
Всё, что мы сделали, это слегка нарушили принцип «Single», произвели декомпозицию монолитного клиентского приложения на несколько отдельных приложений, не потеряв при этом почти ничего. Что мы получили, благодаря этому подходу?
1. Проблема с утечками памяти автоматически решена, так как в процессе работы пользователь перемещается между страницами, что вызывает полную очистку памяти, без каких-либо издержек с нашей стороны.
2. Дополнительные возможности, такие как быстрая загрузка контента по ссылке (без загрузки всего приложения, по крайне мере той его части, которая позволила бы пробраться «внутрь»), а также разный минимальный HTML для каждой из страниц, вместо одного универсального «Loading…»
3. Мы как-то быстро позабыли о том, что у нас есть Razor, и какие чудесные возможности он нам даёт вместо статичного HTML, несмотря на то, что для клиента эти страницы всё равно выглядят как статичные с длительным временем кеширования.
4. Отдельные страницы в итоге позволили развивать эти части приложения независимо, допустим одна страница может падать, при этом остальные продолжат работать как ни в чём не бывало.
Добавлю, что всё вышеописанное достаточно узко специфично, и рассматривалось исключительно в рамках используемых нами технологий и конкретного ПО. Далеко не всегда такой подход будет оправдан и даст хоть какие-либо преимущества перед мощью SPA.
Благодарим за внимание!
Автор: mllm