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

Рассказчики и по совместительству как раз такие борцы — Игорь Зубов и Алексей Шаров, фронтенд тимлиды в Wrike.

Мы работаем в разных командах одного продуктового юнита — Wrike for Professional Services. Одна из самых сложный частей продукта, за которую отвечает наш юнит, — Workload. Workload — это график, который используют менеджеры разных уровней. На нем они могут видеть загрузку и смотреть, в какие дни сотрудники перегружены задачами, а в какие — недозагружены. Еще менеджер может видеть все задачи проектов, планировать, менять их местами или менять человека, на которого они назначены.

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

Workload — это не только календарная сетка, но и проекты и задачи:

Синяя горизонтальная полоска — задача. Она запланирована на четыре дня и находится в проекте “Pyrix” одного из пользователей
Синяя горизонтальная полоска — задача. Она запланирована на четыре дня и находится в проекте “Pyrix” одного из пользователей

Мы создали Workload три года назад как простую таблицу. Сейчас это сложный компонент: больше 100 000 строк кода и огромное количество вычислений на стороне фронтенда (многое делается оптимистически: пересчеты расписания, движение задачи). 

Кейс на первый взгляд кажется простым. Но, например, у пользователя может быть свое рабочее расписание: в один день он работает 8 часов, в другой — 3, а в третий — не работает. На одну задачу может быть назначено сразу несколько пользователей с разным расписанием. Изначально мы решили реализовывать всю логику на фронтенде, поэтому вычислений там очень много. Компонент получился сложным.

В начале 2021-го года мы поняли, что все плохо. 

Как мы поняли, что все плохо

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

Архитектура компонента, которую мы создавали в 2018-ом году, не была масштабируемой и подходила для клиентов Middle-сегмента. Тогда нам не нужно было обрабатывать большие объемы данных. Пока мы добавляли новые фичи, Wrike как продукт и компания сильно вырос. У нас появилось много клиентов уровня Enterprise. А в таких компаниях больше команд, больше людей и больше данных.

Это привело к тому, что в январе 2021-го к нам пришли первые клиенты, которые жаловались на скорость загрузки Workload. У нас было примерно десять эскалаций. Всего клиентов у компании 20 000+, и кажется, что 10 из 20 000 — совсем небольшая цифра. Но эскалации появились за короткий промежуток времени — 10 за один месяц. 

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

Возможные причины проблем с перфомансом

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

Количество DOM-элементов на странице. Таблица сделана на DOM. Большое количество данных — огромное количество DOM-элементов на странице. Мы предположили, что уперлись в производительность Angular’а.

В Workload есть режим compact mode, который увеличивает количество клеток в таблице в два раза:

На этой странице в компактном режиме 49 тысяч DOM Nodes. И это не самое большое количество, которое мы видели. Некоторые пользователи используют 4k-мониторы, и у них информации в таблице в 10 раз больше
На этой странице в компактном режиме 49 тысяч DOM Nodes. И это не самое большое количество, которое мы видели. Некоторые пользователи используют 4k-мониторы, и у них информации в таблице в 10 раз больше

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

Их может быть очень много:

Здесь почти 50 тысяч. Бывало и 70 тысяч
Здесь почти 50 тысяч. Бывало и 70 тысяч

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

Усложненные фичи работают медленнее с увеличением объема данных. Мы пытались сделать продукт максимально удобным для пользователей и ввели излишне усложненные фичи: infinite scroll, optimistic calculation, online updates. С увеличением объема данных эти фичи начали работать медленнее.

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

Один из самых популярных сценариев — пересчет длительности задачи согласно календарю пользователя. Когда пользователь собирается переместить задачу с одного дня на другой, мы должны отрисовать специальный плейсхолдер с предполагаемой длительностью задачи и указать день начала и конца. При этом календарь пользователя может быть совершенно произвольным: два через два, сокращенные дни, полставки и полдня. Все это нужно учитывать во время «таскания» задачи по Workload. А еще — добавление или удаление группировок, оно тоже производится на фронтенде.

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

План действий для решения проблемы

Чтобы разобраться, что происходит на самом деле, мы составили план действий на месяц.

Выглядел он так:

  1. Понять, что происходит у клиентов. 

  2. Улучшить их жизнь за месяц с помощью быстрых фиксов.

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

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

Как мы поняли, что все тормозит именно на стороне клиентов? Ведь Support-тикеты обычно очень лаконичны: там есть минимальное описание проблемы, несколько скриншотов и видео, если повезет. 

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

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

Чтобы получить объективную оценку о том, насколько сильно в действительности тормозит наше приложение, мы начали использовать систему Apdex — Application Performance Index. 

Performance Tracking и система Apdex

Наверное, многие читатели знакомы с трекингом. Это то, что собирают аналитики. Например, пользователь нажимает на кнопку A — tracking event уходит в аналитику. Аналитики видят, что на кнопку А пользователь нажимает часто, а на кнопку B не нажимает совсем. Мы использовали такой трекинг, чтобы посмотреть, насколько долго выполняются пользовательские действия, фактически создав Performance Tracking.

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

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

Для замера мы отправляли два трекинг ивента. Первый — в тот момент, когда пользователь выполнил действие:

{"event":"row__expand","datetime":"2021-05-01T17:40:43.758","group":"performance","value":{"workload_id":"81451","members_cnt":1,"zoom_level":"dayDense","tasks":6,"grouping":"jobRoleGrouping/user/project","performance_uuid":"972438e0-aa93-11eb-a8f9-557874958202","performance_timestamp":1619883643758,"performance_event_type":"start"},"version":"2021-03-12"}

Второй — когда действие, которое выполнил пользователь, закончило отрисовываться:

{"event":"row__expand","fraction":"1/1","datetime":"2021-05-01T17:40:43.953","group":"performance",,"value":{"performance_uuid":"972438e0-aa93-11eb-a8f9-557874958202","performance_timestamp":1619883643953,"performance_event_type":"finish"}

По разнице между метками мы можем понять, сколько времени выполняется каждое действие. В нашем примере получается: 1619883643953 - 1619883643758 = 195ms.

Возникает резонный вопрос: а 195 миллисекунд — это много или нормально? Ответить на него нам помогла система Apdex. 

Методика Apdex — это открытый международный стандарт, разработанный с целью формирования объективной оценки показателей производительности информационных систем. Индекс — это число от 0 до 1: значение 1 означает, что приложение работает идеально, 0 — приложение не работает совсем.

Чтобы получить это число, нужно выполнить ряд действий:

  • Подготовить список операций, которые нужно отслеживать. Мы открыли Workload и начали думать о том, какие операции обычно совершают пользователи: поскроллить, кликнуть и отобразить tooltip с задачей, открыть бэклог.

  • Задать для каждой операции целевое время: какой промежуток считать хорошим, приемлемым и неприемлемым. Действия разные, поэтому для каждого нужно определить свой промежуток. Первоначальная загрузка может выполняться несколько секунд: это может удивить пользователя, но после открытия таблицы он забудет о том, что ему пришлось подождать две секунды. А если будет тормозить процесс движения задачи — это плохо, потому что такое действие пользователь выполняет часто. Для первоначальной загрузки хорошее и приемлемое время — 1-2 секунды, для движения задачи — 200-500 миллисекунды.

  • Разбить события по приоритетам: high, mid, low. Первоначальной загрузке, скорее всего, не стоит ставить высокий приоритет, потому что она выполняется один раз. А вот постоянные действия должны быть в высоком приоритете.

  • Включить счетчик замера производительности для всех операций из списка и накопить статистику.

Для каждой операции мы получили время ее выполнения, которое попадает в определенный промежуток.

Apdex считается по формуле:

Складываем количество ивентов, которые попали в промежуток «хорошо», и количество ивентов, которые попали в «приемлемо», поделенное на два. Делим получившуюся сумму на общее количество ивентов
Складываем количество ивентов, которые попали в промежуток «хорошо», и количество ивентов, которые попали в «приемлемо», поделенное на два. Делим получившуюся сумму на общее количество ивентов

Получившееся значение индекса оценивается по таблице:

Мы собрали статистику по нашим ивентам в Tableau:

Несколько месяцев значение индекса не изменялось
Несколько месяцев значение индекса не изменялось

Нам удалось выяснить, что индекс зависит от количества строк в Workload — количества пользователей, которые отображаются на странице:

Если количество пользователей от одного до четырех, то все работает достаточно быстро. Когда пользователей больше 10, то таблица начинает тормозить
Если количество пользователей от одного до четырех, то все работает достаточно быстро. Когда пользователей больше 10, то таблица начинает тормозить

Наша гипотеза подтвердилась: увеличение количества данных негативно влияет на скорость загрузки.

Как мы решаем проблему с перформансом: быстрые фиксы и долгосрочные планы

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

К быстрым фиксам мы отнесли следующие попытки.

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

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

Бэклог — это нижняя панель в Workload. Она отображает задачи, которые не назначены на исполнителей:

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

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

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

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

Мы включили новую функциональность нескольким клиентам. Скорость загрузки значительно улучшилась, но клиентам не понравилось — оказалось, что им важно видеть задачи во время скролла. Эксперимент получился неудачным.

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

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

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

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

Для среднесрочных и долгосрочных решений мы придумали два выхода:

  • Запрототипировали и запланировали перевод продукта с DOM на Canvas. Прототип показал, что на Canvas работать все будет значительно быстрее. 

  • Запланировали уход от горизонтального Infinite scroll. Мы решили использовать обычный браузерный скролл и загружать данные по запросу. Когда пользователь упрется в границу загруженного time frame, то сможет подгрузить новые данные. Во многих похожих продуктах на рынке используется тот же механизм. Видимо, они что-то узнали раньше нас.

Эти два решения должны позволить нам на ближайшие год-два решить все проблемы с отрисовкой.

Результаты и вывод

После фиксов значение Apdex поменялось:

Первые эксперименты мы начали выкатывать в середине марта. К концу апреля/началу мая наш Apdex вырос до значения 0,80. Это еще не хорошо, но уже очень близко
Первые эксперименты мы начали выкатывать в середине марта. К концу апреля/началу мая наш Apdex вырос до значения 0,80. Это еще не хорошо, но уже очень близко

График индекса в зависимости от количества пользователей:

Зависимость осталась: чем больше пользователей, тем больше тормозит таблица. Но если раньше в аккаунтах с количеством пользователей 20+ Apdex был 0,60, то сейчас — 0,75
Зависимость осталась: чем больше пользователей, тем больше тормозит таблица. Но если раньше в аккаунтах с количеством пользователей 20+ Apdex был 0,60, то сейчас — 0,75

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

Главный вывод, который мы сделали после этой ситуации — перформанс на первом месте. Банальная прописная истина: инженер должен следить за тем, чтобы не срезались углы. Если есть подозрения, что будет работать медленно, то задача инженеров — сигнализировать и говорить об этом бизнесу. 

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