Как мы создаем Squadus: проблемы фронтенда и пути их решения

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

Мы продолжаем рассказывать о разработке недавно вышедшего продукта.

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

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


Привет, Хабр! Меня зовут Виталий Матвиевич, я руководитель группы разработки в МойОфис. Вместе с командой мы создаем Squadus — цифровое рабочее пространство, которое позволяет компаниям гибко структурировать коммуникации. Ранее мой коллега @zasonnikрассказал о бэкенде продукта. Я же, в свою очередь, остановлюсь на пользовательской части Squadus: нюансах разработки фронтенда на базе open-source, сопутствующих проблемах и способах их решения.

С чем предстояло справиться

Как мы уже писали, разработка Squadus начиналась на базе СПО. Пару лет назад мы взяли основную функциональность из Rocket.Chat, но быстро поняли, что нашим потенциальным заказчикам — компаниям разной численности — этих возможностей будет недостаточно. Так мы начали переписывать решение. В процессе выявили массу проблем и последние полтора года активно с ними разбирались

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

  1. Технический долг. Rocket.Chat использует fullstack-фреймворк Meteor.js, в чем-то удобный, но вместе с тем привносящий в проект ряд проблем. Наиболее острая из них: шаблонизатор Blaze, на котором в Rocket.Chat были реализованы ключевые компоненты, включая комнаты, чаты, сообщения и т.д. А это — jQuery, отсутствие TypeScript и уступающая React-компонентам производительность. Справедливости ради отмечу, что команда Rocket.Chat также активно уходит от Blaze и переводит компонентную базу на React и TypeScript.

    Еще один комплекс проблем: вместо библиотек, рекомендуемых разработчиками Meteor.js для работы с React-компонентами, создатели Rocket.Chat использовали собственные решения. Как показала практика, это стало причиной некоторых плавающих багов.

  2. Проблемы с UI-kit. React-компоненты в Rocket.Chat реализованы на основе собственной библиотеки (RocketChat Fuselage). С одной стороны, она дает большой набор готовых компонентов и предоставляет инструменты для гибкой разработки новых компонентов, с другой стороны — привносит ряд ограничений. Наиболее проблемными компонентами были поля ввода и </p>" data-abbr="UI-компоненты для поиска и выбора элементов из выпадающего списка. Включает в том числе возможности по множественному выбору элементов, при необходимости интегрируется с API для получения данных.">саджестеры.

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

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

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

Что мы предприняли

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

  1. Рефакторинг компонентов с Blaze на React и TypeScript

  2. Перевод UI-компонентов с Fuselage на собственный UI-kit

  3. Оптимизация приложения

Расскажем немного о каждом направлении.

Рефакторинг компонентов с Blaze на React и TypeScript

Уход с Blaze на React и TypeScript помогает нам достичь сразу нескольких целей:

  • Удалить из проекта лишние зависимости — в частности, сам Blaze и jQuery, некоторые другие пакеты.

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

  • Повысить производительность приложения и исправить ситуацию с утечками памяти. React-компоненты показывают лучшую производительность, при этом реализованный в Rocket.Chat механизм для рендера Blaze-шаблонов в React (и наоборот) — течет.

  • Устранить сопутствующие баги в legacy-компонентах.

Хороший пример подобной рефакторинговой задачи — лента чата. С ней был связан большой спектр проблем:

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

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

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

  • Некорректный переход к сообщению по ссылке из-за скачущей высоты компонентов и некорректной обработки этих сценариев.

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

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

  • Отсутствие пересылки сообщений.

  • Отсутствие черновиков сообщений.

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

За последние полгода мы провели большой объем работ над оптимизацией ленты и решили следующие задачи:

  • Реализовали механизм виртуализации сообщений и infinite scroll, тем самым устранили проблемы пользователей при работе с историей чатов.

  • Написали менеджер истории для тредов, также прикрутив механизм виртуализации и infinite scroll.

  • При помощи IntersectionObserver реализовали механизм отложенной «активации» сообщений и вынесли туда все тяжелые операции, включая отображение медиа.

  • Научили ленту открывать чат с первого непрочитанного сообщения, а также при помощи той же механики на IntersectionObserver научились «читать» пользователем только те сообщения, которые попали во вьюпорт.

  • Стали производить склейку и вывод дат до рендера сообщений (в актуальной версии RocketChat эта проблема тоже решена).

  • Добавили пересылку сообщений (включая массовую пересылку).

  • Добавили черновики сообщений. Правда, пока без синхронизации между устройствами.

  • Починили лимиты на кэширование комнат.

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

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

UI-Kit

Теперь поговорим про уход с Fuselage на собственный UI-kit. В решении этой задачи нам помогает опыт коллег из команды разработки Mailion. Совместно мы разрабатываем и поддерживаем так называемый базовый UI-kit, который сделан на основе MUI и предоставляет командам обоих продуктов базовый набор компонентов.

На основе этих общих компонентов мы создаем UI-kit Squadus. Такой подход позволяет нам синхронизировать подходы к разработке базовых компонентов с другими командами, переиспользовать решения друг друга, но не приводит к ограничениям по кастомизации компонентов внутри нашего продукта. На уровне продуктового кита (UI-kit Squadus) мы подставляем в компоненты цветовую схему продукта из Figma.

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

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

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

Оптимизация приложения

В рамках работ по оптимизации приложения мы наметили следующие задачи:

Уменьшение бандла

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

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

Устраняем проблемы с кэшированием и соединением

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

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

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

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

Что мы сделали:

Как мы справлялись с утечками памяти

Отдельным направлением работ по улучшению пользовательского опыта в нашем продукте стала борьба с утечками памяти. Честно говоря, никто из команды разработки не обладал «боевым опытом» по поиску утечек. Конечно, у всех было общее понимание: что это, откуда оно берется, но как методологически выстроить работу по планомерному выявлению и починке — с этим нам предстояло разбираться.

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

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

Однако снапшоты показали, что в памяти остаются ВСЕ чаты, которые открывал пользователь. Естественно, за день активной работы память выедалась страшными объемами.

С точки зрения кода, на первый взгляд, все было нормально, но в итоге мы обнаружили лишний символ «!» в методе, который удаляет комнату из кэша:

close(rid: IRoom['_id']): void {
    if (!this.rooms.has(rid)) {
      this.rooms.delete(rid);
      this.emit('closed', rid);
    }
    this.emit('changed', this.rid);
  }

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

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

***

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

Источник: https://habr.com/ru/companies/ncloudtech/articles/731734/


Интересные статьи

Интересные статьи

Если вы хоть раз настраивали MPLS L3VPN, то у вас не возникнет сомнения в том, что весь подход вертится вокруг BGP. Будучи протоколом маршрутизации с развитым чувством собственного достоинства (в конц...
Дмитрий Стогов из Zend by Perforce уже много лет занимается самым сердцем PHP и знает про него много полезного. В том числе о вопросах, связанных с производительностью.В ...
Определение TTL для некоторых кэшированных данных («time to live» — время существования или длительность хранения) может стать своего рода шарлатанской нумерологией для п...
ИИ нужно бегать по разным навигационным картам, каждая из которых требует свои входные и выходные данные, но прописывать дополнительную логику не хочется? Отлично, в этом вопросе нам пом...
Предлагаем вашему вниманию подборку с ссылками на новые материалы из области фронтенда и около него. Читать дальше →