Коль скоро я являюсь фуллстек JavaScript-разработчиком (если совсем точно, то TypeScript-разработчиком), я решил испытать себя. Манил не только призовой фонд, но и сам формат: это не соревнования по программированию, где важны абстрактность и скорость мышления. Здесь было важно всё в комплексе: опыт, скорость разработки в среднесрочной перспективе, вкус в вопросах UI, знание computer science в целом, самокритичность. По условиям конкурса необходимо было разработать библиотеку для отображения графиков для одной из платформ: iOS, Android или Web.
Разработчики для разных платформ не конкурировали между собой, и у каждой платформы победители были свои. Основными критериями были: скорость работы (в том числе и на старых устройствах), соответствие дизайну, плавность анимации и минимальный размер приложения. Уже существующие решения и библиотеки использовать было нельзя, всё должно было быть написано с нуля.
До этого я участвовал в конкурсах для разработчиков, где на все задачи выделялось не более 5 часов, эти часы приходилось проводить в огромном напряжении. Несмотря на то, что выполнение задачи в конкурсе от Телеграма не требовало такого напряжения, это один из самых сложных конкурсов, в которых мне приходилось участвовать. С виду несложная задача оказалась настолько ёмкой, что если бы мне за это платили, я бы мог пилить эти «графики» месяцами, пытаясь найти компромисс между производительностью кода и архитектурной его стройностью. Выручало то, что на решение выделялось три (upd: две, спасибо vlad2711 за поправку) недели. Некоторые из соперников специально брали отпуск, чтобы уделить конкурсу больше времени, а я решил совмещать разработку для конкурса по вечерам и выходным с работой в "Онланте" в обычном режиме.
CANVAS versus SVG
Самый главный архитектурный вопрос, вставший перед всеми нами, был в выборе инструмента отрисовки графики. На текущий момент веб-стандарты предлагают нам два подхода: через генерацию «на лету» svg-графики и старый добрый canvas. Вот плюсы и минусы каждого из них.
Canvas
+ Абсолютная универсальность — имея возможность изменить цвет любого пикселя на полотне, можно нарисовать всё, что угодно.
+ [Потенциально] Высокая производительность — если уметь готовить canvas, он может показывать неплохую производительность. Было бы замечательно использовать webgl, но его поддержка на смартфонах оставляет желать лучшего.
- Все расчёты и вся отрисовка вручную — в отличие от SVG, где промежуточные точки ломаной можно задать единожды, а далее можно манипулировать viewbox-ом для перемещения «камеры» по участкам ломаной, с canvas всё сложнее: никаких «камер» тут нет, есть только координаты от левого верхнего угла; если нужно «переместить» текущую область просмотра графика, необходимо заново рассчитать все координаты всех его точек относительно новой позиции области просмотра. Другими словами viewbox, который в svg есть из коробки, в canvas нужно реализовывать вручную.
- Вся анимация вручную — исходя из предыдущего пункта, все возможные анимации реализуются посредством пересчёта координат, значений цвета и прозрачности и перерисовке всей сцены N-е количество раз в секунду, и чем большее количество раз удалось пересчитать и перерисовать сцену, тем плавнее анимация.
SVG
+ Простая отрисовка — достаточно один раз добавить в SVG необходимые линии, фигуры и далее можно, манипулируя viewport, параметрами цвета и прозрачности, обеспечить навигацию по графикам.
+ Простая реализация анимаций — опять же, исходя из предыдущего пункта, достаточно N-e количество раз в секунду указать новые значения для viewbox, цвета и прозрачности, а изображение перерисуется само, об этом позаботится браузер. Кроме того, не стоит забывать, что фигуры и примитивы в SVG можно стилизовать в CSS, поэтому их можно анимировать с помощью CSS3-анимаций, что открывает широчайшие возможности для получения крутых анимаций с минимальными усилиями.
+ Неплохая производительность по умолчанию — если с canvas можно легко, что называется «в лоб», накодить что-то медленное и жрущее сотни ресурсов, то результат, основанный на SVG всегда будет выглядеть вполне легковесным, приличным и плавным.
Но есть и обратная сторона медали.
- Скромные возможности для оптимизации — поскольку svg рисуем не мы, а браузер, то и контролировать этот процесс невозможно — если хочется увеличить производительность, например, за счёт кэширования уже отдельных отрисованных элементов, сделать это нельзя никак. Скорее всего это уже делает браузер, но мы не можем быть уверены до конца.
- Ограниченность инструментария — в SVG мы уже не контролируем каждый пиксель полотна, а думаем и кодим в рамках векторных примитивов. Впрочем, для этой задачи это несущественный минус, накладывающий некоторые, опять же несущественные ограничения в контексте задачи конкурса.
Выбором инструмента мучиться мне не приходилось никогда, поскольку у меня есть отвратительная черта характера — я максималист и привык использовать в работе только любимый инструмент. Так получилось, что еще со студенческих времён, когда я забавлялся с DirectDraw, любимым моим инструментом всегда было полотно, на котором «делай что хочешь». И canvas для решения конкурсной задачи действительно оказался хорош, но по-настоящему сыграл мне на руку лишь один его плюс: широчайшие возможности для оптимизаций, поскольку основным критерием была всё-таки производительность приложения.
Хороший код нехороший
Задача ясна: нужно рисовать точки на полотне в нужном месте и в нужное время. Осталось написать код. Снова нужно было выбирать, на этот раз между написанием производительного компактного кода одной «портянкой» в процедурном стиле или не очень производительного и уж тем более некомпактного в моём любимом объектно-ориентированном. Наверное, вы уже догадались, что я выбрал второй вариант, приправив его ещё одним моим любимцем — TypeScript.
И этот выбор оказался не очень правильным. Из-за использования абстракций и инкапсулирования не везде получается сохранять, передавать и повторно использовать промежуточные результаты вычислений, что плохо сказывается на производительности. А из-за повсеместного использования this, без которого ООП в JS невозможен, код плохо минифицируется, тогда как размер тоже имел значение.
Настало время дать ссылку на гитхаб: github.com/native-elements/telechart. Если интересно, рекомендую обратить внимание на историю коммитов, она хранит память об оптимизационных мытарствах и небезуспешных попытках выжать пару лишних кадров отрисовки в секунду.
Ну а в конкурсе я не занял призового места. И проблема, как это часто с нами-программистами бывает, оказалась не в недостаточном опыте, сообразительности или скорости, а в недостаточной самокритичности: сам факт того, что у меня получилось сделать, оно работает и выглядит как на картинке, меня порадовал, а по поводу тормозов отрисовки я думал, что я сделал всё, что мог, у остальных наверняка так же. Стыдно об этом говорить, но я был уверен, что займу первое-второе место. На деле же оказалось, что я написал тормозную и глючную программу, не самую плохую, но и далеко не самую хорошую. Когда я увидел работы других разработчиков, понял что у меня нет шансов и оставалось только кусать локти. Будь я беспристрастен к своему труду, я бы занялся производительностью, самой важной частью конкурсного задания.
Один из ценнейших уроков в моей профессиональной жизни, который я не устаю получать, заключается в том, что хороший инженер в отличие, например, от художника, обязан объективно оценивать качество своей работы, отбросив самоуверенность, потому что результат его труда должен не только глаз радовать, но должен правильно и хорошо работать.
Это был первый этап конкурса. Победители были щедро вознаграждены. К моей неописуемой радости на этом история не закончилась, потому что был анонсирован второй этап:
Необходимо было доработать свою поделку, всего лишь за неделю реализовав дополнительные типы графиков. Покажу сразу, что получилось, а ниже расскажу как это получилось.
В моём случае, прежде чем добавлять новую функциональность, нужно было разобраться с производительностью старой. Первая проблема, которую я решил — это
Дёрганая анимация
Даже если вам хватает мощностей, чтобы выдавать 60 кадров в секунду, анимация не будет плавной, если положение элемента или его прозрачность не детерминированы временем, прошедшим с начала анимации. Это обусловлено неравными промежутками времени между тиками: например, один тик сработал через 10 мс, а второй — через 40, в то время как и за первый, и за второй тики объект переместился влево на 1 пиксель — то есть скорость его перемещения постоянно плавает, визуально это выглядит как «подёргивание». Иными словами, нужно делать не так:
let left = 10, interval = setInterval(() => {
left += 1
if (left >= 90) {
clearInterval(interval)
}
}, 10)
А так:
let left = 10, startLeft = 10, targetLeft = 90, startTime = Date.now(), duration = 1000, interval = setInterval(() => {
left = startLeft + (targetLeft - startLeft) * (Date.now() - startTime) / duration
if (left >= targetLeft) {
left = targetLeft
clearInterval(interval)
}
})
Поскольку анимируемых параметров в коде много, я запилил универсальный класс, который облегчает задачу, да ещё и добавляет изинг к анимации. Он достаточно прост в использовании:
let left = Telemation.create(10, 90, 1000)
…
drawVerticalLine(left.value) // В любое время здесь будет нужное, детерминированное значение.
Дальше в игру вступает правило 60 fps. ПК-геймеры меня поймут: чтобы анимация выглядела идеально, она должна отрисовываться со скоростью не менее 60 fps. Соответственно, каждая отрисовка кадра должна занимать не более 1/60 секунды. Для этого нужно мощное железо и хороший код.
Дальнейшие изыскания показали, что
Прорисовка canvas тормозит, если над canvas есть html-элементы.
Изначально я использовал «пустые» html-элементы для того, чтобы реализовать управление текущей областью просмотра:
Эти элементы располагались поверх canvas, и несмотря на то, что у них не было никакого контента, они использовались только для отслеживания событий мыши, в результате экспериментов выяснилось, что само их наличие снижает производительность отрисовки. Убрав их и немного усложнив логику определения событий управления областью просмотра, я увеличил скорость отрисовки кадра.
Оставалось выдернуть последний гвоздь из крышки гроба производительности: я сделал
Кэширование миникарты
До этого для миникарты линии отрисовывались каждый кадр заново. Это дорогая операция, потому что на ней отображался весь график за год (365 точек на каждую линию). Очевидным решением, которое я просто поленился реализовать с самого начала, было однократное отрисовывание линий графика для миникарты, сохранение результата в кэш и использование этого кэша в дальнейшем. После этой оптимизации за производительность приложения перестало быть стыдно.
Дальше что?
Было ещё много успешных и не очень драк за производительность: попытки кэшировать результаты вычислений координат, эксперименты с параметрами lineJoin у CanvasRenderingContext2D (miter быстрее), но они не так интересны, так как не давали заметного выигрыша в производительности либо не давали его вообще.
Из восьми дней пять я потратил на ускорение кода и только три — на допиливание новой функциональности. Да, мне хватило всего три дня, чтобы добавить новые типы графиков, и тут весьма кстати оказался ООП, с ним кодовая база увеличилась незначительно. Мне не хватило времени, чтобы выполнить бонусное задание (ещё +5 дополнительных графиков). Полагаю, что те пять дней, которые я потратил на устранение последствий моей уверенности в себе, я мог потратить на решение бонусной задачи.
Тем не менее мои труды дали результат: 4-е место и «утешительный» приз в одну тысячу долларов:
Кстати, конкурс продолжился дальше, но уже без меня.
Я доволен участием: кроме того, что это просто интересно и является интересным приключением, я получил хороший профессиональный опыт и жизненный урок.
Кроме того, эту библиотеку я использовал в разработке нашего корпоративного таймтрекера, о котором тоже планирую рассказать на Хабре в ближайшее время.
Для обсуждения предлагаю такой вопрос: зачем Телеграму это всё нужно? Я считаю, что за адекватные деньги Телеграм получит самую лучшую в мире библиотеку для отображения графиков: лучший результат из сотен попыток сделать лучше, чем у других. Соревновательный принцип позволяет получить настолько высокий уровень качества, который на заказ не способен сделать никто и ни за какие деньги.
И немного ссылок:
- Результаты конкурса: t.me/contest/81
- Все участники конкурса: contest.dev/chart-js
- Моя страница на сайте конкурса: contest.dev/chart-js/entry74
- Моё приложение: jschart.usercontent.dev/entry74
- Если нет Телеграма, можно посмотреть тут: asyncoders.com/telechart/index.html
Комментарии (46)
iago
06.08.2019 16:15+2Поздно я узнал об этом конкурсе. Автор, вы молодец, и слог очень понравился как пишете, все понятно и по полочкам. Единственное, не понял к чему в конце список каких-то вакансий, в свободное от хакатонов время HR-ом подрабатываете?
ShibaOn Автор
06.08.2019 16:29+1Спасибо за отзыв :-) Заметка опубликована в блоге компании ГК ЛАНИТ, где я имею счастье работать
ка краб на галерахуже джва года, поэтому коллеги из отдела HR не упустили возможность «попиарить» горячие вакансии.
P.S. Если что, нам, а точнее мне в команду, так же ОЧЕНЬ нужен Fullstack JavaScript разработчик.iago
06.08.2019 16:52+2Понятно, не обратил внимание что блог корпоративный, сильный блог для корпоративного сегмента! Я, к сожалению или к счастью, iOS developer :)
TheGodfather
06.08.2019 16:24+5>Я считаю, что за адекватные деньги Телеграм получит самую лучшую в мире библиотеку для отображения графиков: лучший результат из сотен попыток сделать лучше, чем у других. Соревновательный принцип позволяет получить настолько высокий уровень качества, который на заказ не способен сделать никто и ни за какие деньги.
Вы же знаете про вечный баланс олимпиадников-рокстаров и инженеров-середнячков. Вы не получите «высокий уровень качества» и «лучший результат». Точнее, результат может хорошо работать и быть производительным, но очень маловероятно, что он будет maintainable и production-quality.ShibaOn Автор
06.08.2019 16:48+1Я сам считаю, что из «олимпиадников» боевые единицы для разработки прикладного ПО так себе — возможно это просто моя зависть, потому что я на олимпиадах показывал средние, если не сказать плохие, результаты :-)
Однако в этом конкурсе всё же своеобразный подход: он проходил в несколько этапов и задачи ставились таким образом, что плохая архитектура должна была бы привести
участника в тупик, образованный невозможностью сопровождать собственный же код. Опять же, у Дурова это не первый опыт получения кода для своих программ на конкурсной основе, полагаю он видит в этом смысл.
Жаль, что при оценке приложений судьи не обращали внимания на исходники, но, думаю, перед тем, как выбрать код, который нужно влить в свой репозиторий, они будут руководствоваться в том числе и критериями качества программ, благо выбирать им есть из чего.SemenPV
06.08.2019 20:05По условиям конкурса Телеграм имеет право использовать любой код который к ним был послан? Мне казалось что подобное не должно быть возможным, может конечно в Саудовской Аравии это ок, но думаю что это черевато судебными исками. Зачем им это надо, если они могут любую профессионально написанную библиотеку подтянуть?
ShibaOn Автор
06.08.2019 22:05Насчёт любой — сомнительно, не помню, чтобы в условиях где-то такое было написано. Но перед тем как мне перечислить деньги, бумажку, в которой я выражаю согласие на использование моего кода, попросили подписать.
iago
06.08.2019 16:56+1Подпишусь под каждым словом, более того, копая исходники open source либ, я всегда видел почему сделан тот или иной костыль. И любой совершенный изначально код обрастает костылями.
То же и про производительность. Помню, вышел Chrome году в 2009-м, о боже как он быстро работал, все сразу стали на него переходить. А потом, когда его функциональность сравнялась с тем же фаерфоксом, разрыв стал уже далеко не таким впечатляющим.
Если компонент останется достаточно простым, все будет хорошо, но стоит прикрутить 20 видов анимированных маркеров, разную универсальность, поддержку тем и т.п., станет таким же как все остальныеKwisatz
06.08.2019 21:41Ах еслиб еще можно было открутить, цены бы решений этим небыло. Но у некоторых либ приходиться с мясом вырывать эти костыли
karabas_b
07.08.2019 04:53любые фичи несложно сделать подключаемыми и отключаемыми. засада в том, что каждая подключаемая или отключаемая фича это минимум одна, а то и несколько дополнительных опций в настройках. а чем больше у программы опций, тем более сложно и пугающе она выглядит для среднестатистического юзера. в общем, практика показывает, что юзеры намного легче мирятся с тормозами, чем со сложными для них настройками, поэтому тенденция на пихание всех фич по дефолту без явной возможности отключения или оптимизации будет скорее всего только нарастать.
Cerberuser
07.08.2019 07:24А если речь о библиотеках, которыми пользуются разработчики, а не о программах для конечных юзеров?
JSmitty
07.08.2019 09:23Разработчики тоже обычно не любители пространных конфигов. Популярность Parcel тому примером.
Cerberuser
07.08.2019 10:16То есть, сделать грамотный конфиг единственно возможным — это нормально и правильно, а сделать тот же самый конфиг конфигом по умолчанию, но дать возможность его менять, — это уже сложно, страшно и всех распугает?
ReklatsMasters
06.08.2019 19:03Уже существующие решения и библиотеки использовать было нельзя, всё должно было быть написано с нуля.
Вот этот бред в головах дуровых идёт с самого вк. Вместо существующих решений постоянно какие-то низкокачественные велосипеды. Тот же вк миру веб разработки не дал НИЧЕГО несмотря на то, что у них было всё своё. При этом дуров гордо рапортовал, что нанимает самых лучших разработчиков. И пофиг, что весь вк того времени был монолит, а js был ужаснейшим лапшекодом с глобальными переменными.
psFitz
06.08.2019 19:44а что он должен был вам дать? Хоть там был и лапшекод, но он работал очень быстро, особенно на фоне фейсбука, который дал миру react
ReklatsMasters
07.08.2019 00:05Сравнивать вк и фб не очень корректно из-за размера аудитории, объёма доходов и т.д. Реакт очень сильно помог снизить затраты фб на поддержку и увеличить скорость внедрения фич. По вк такой инфы нет, но я могу судить по себе: входить входить в крупный монолитный проект с кучей глобальных переменных и стейтов очень сложно.
psFitz
07.08.2019 09:30+1при чем тут размер аудитории? Я сравниваю по тому, как быстро работает фронтенд, это никак не относится к размеру аудитории. Да фб зарабатывает больше денег, но это обусловлено 2 факторами:
- Контакт заточен под снг, а фб под весь мир
- На фб интерфейс построен таким образом, что-бы для простого действия надо было больше экранов видеть, для показа рекламы на этих экранах
mad_god
07.08.2019 10:45эмм,
В конце мая, перед началом сезона отпусков, суточная посещаемость ВКонтакте достигла очередного рекорда — почти 50 млн. пользователей. Именно тогда мы завершили перевод всего кода ВКонтакте на компилируемый язык программирования, который разрабатывали более года, — KPHP. В результате практически все страницы сайта стали грузиться более чем 2 раза быстрее.
TheGodfather
07.08.2019 11:19Тем не менее, в итоге все-таки ВК работает на порядок шустрее и приятнее ФБ. Вполне возможно, что говнокод от олимпиадников, который не очень поддерживаемый, но при этом работает :) Мне как пользователю важнее, что сайт работает быстро, нежели что говнокод внутри. Баланс такой баланс…
RomanArzumanyan
07.08.2019 11:54Vk — это коммерческая компания, целью которой является получение прибыли.
Они её таки получали. Разработка программного продукта была средством получения, а не целью.
Уже существующие решения и библиотеки использовать было нельзя, всё должно было быть написано с нуля
Это совершенно здоровый подход в случае замкнутой на саму себя экосистемы. Используется в геймдеве под консоли повсеместно.
Vk был новой предметной областью для применения существующих подходов.
anatoliy841993
06.08.2019 20:21+4Почему используете setInterval когда для плавных анимаций есть requestAnimationaframe?
ShibaOn Автор
06.08.2019 21:57requestAnimationFrame для рендеринга, поскольку в примерах рендеринга нет, решил не усложнять.
anatoliy841993
07.08.2019 12:43+1не совсем с вами согласен, это может быть полезно не только с рендерингом, React давно использует raf, вот пример из RN
facebook.github.io/react-native/docs/performance#my-touchablex-view-isn-t-very-responsiveShibaOn Автор
07.08.2019 12:54Вероятно, я в данном вопросе проявляю некоторую архаичность, поэтому мне стоит присмотреться к requestAnimationFrame, так что спасибо за ссылку :-)
webschik
07.08.2019 15:05+1Вот еще одна хорошая демонстрация того, где можно испрользовать raf не только для анимаций github.com/wilsonpage/fastdom
Barbaresk
06.08.2019 21:19А есть где-то инфа по распределениям Canvas/SVG по местам победителей? После информации о велосипедостроении и js в full-стеке, был уверен, что автор выберет Canvas, но интересно, что выбрали другие.
ShibaOn Автор
06.08.2019 22:02Статистики такой нет, при желании можно её собрать вручную, но в телеграмм-чатеге конкурсантов на первом этапе конкурса обсуждался исход участников с SVG в сторону Canvas из-за неудовлетворительной производительности. Хотя мне тогда казалось, что SVG всё-таки пошустрее. Субъективно, среди лидеров конкурса Canvas преобладает.
Kwisatz
06.08.2019 21:40+2Очень круто. Прямо захотелось утащить пару кусков себе, жалко граффики редко юзаю.
Очень радостно видеть, что еще остались разработчики, которые заботятся о быстродействии и удобстве.
altmind
06.08.2019 23:12Участники могут публиковать исходники? Я бы посмотрел на организацию кода в разных вариантах.
Finesse
07.08.2019 09:12+1Отправка решения в виде необфусцированного кода — одно из условий второго тура. Решения и соответствующий код можно посмотреть здесь: https://contest.dev/chart-js
psFitz
Странно, у победителя конкурса ползунки — это html над canvas)
ShibaOn Автор
Описаная проблема возникает из-за того, что перерисовка канваса вызывает необходимость перерисовать и html-элементы, находящиеся над ним, вместе со сглаживанием и полупрозрачностью. Когда нужно перерисовать canvas 60 раз в секунду (например, при перемещении ползунков), издержки, связанные с перерисовкой html-элементов над canvas становятся заметными.
У автора jschart.usercontent.dev/entry81 отдельный канвас для миникарты и он не перерисовывается при перемещении ползунков, потому что сами ползунки сделаны на css. Соответственно, и проблема не актуальна. В целом, такой подход и производительнее и проще в реализации — нужно стараться отрисовывать canvas как можно реже, по-возможности перекладывая рендеринг на html-движок.
psFitz
Спасибо за подробный и полезный ответ)