WebZ


Гео и язык канала: не указан, не указан
Категория: не указана


Пишу о том, как делалось приложение webz.telegram.org.
English: @webzchannel

Связанные каналы

Гео и язык канала
не указан, не указан
Категория
не указана
Статистика
Фильтр публикаций


🎉 Тем временем сегодня состоялся официальный production-релиз!

По адресу web.telegram.org теперь открывается одна из двух новых версий: WebZ или WebK.

Выбор версии при первом заходе произойдёт случайным образом и запомнится, однако в меню можно будет переключиться на предпочитаемую версию (Z, разумеется).

Впереди ещё много интересного.

Буду рад отзывам в комментариях.




Если TDLib-версия была заранее обречена, то версия на GramJS оставляла возможность побороться за главный приз.

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

Пришлось погрузиться в глубины реализации MTProto, чтобы расставить повсюду console.time. Спустя несколько дней проблема нашлась, хоть и была зарыта глубоко. В одной из функций шифрования использовался spread-оператор ... внутри цикла for, что приводило к квадратичной вычислительной сложности. Простая замена на concat, и приложение «взлетело»: скорость передачи данных выросла в 12 раз.

Позже я ещё раз наткнулся на похожую утечку уже в «своей» UI-части — некий большой массив данных формировался из исходного с помощью reduce, внутри которого была запись return [...acc, newValue], что также порождало квадратичную сложность и замедляло рендеринг. В общем, не надо так. Если хочется сохранить лаконичную запись, можно использовать такой хелпер:

```
// Faster than `[...array, member]`
export function copyAndPush(array, member) {
return array.concat([member]);
}
```

Исправление в GramJS заинтересовало автора библиотеки, и он согласился помочь с её доработкой для конкурса. Одним из главных критериев конкурса — speed, size and attention to detail — было сохранение небольшого размера сборки. Поэтому следующей большой задачей было сокращение размера GramJS, который на тот момент добавлял почти мегабайт к минифицированному JS-бандлу.

Продолжение следует.


Как всегда бывает, путей решения проблемы существовало несколько.

Первый. Собственноручная реализация MTProto на JavaScript с нуля — самый сложный и рискованный путь. Несмотря на это, его выбрали команды, которые заняли призовые места (Hip Hyena, Giant Parrot и Posh Ram). Giant Parrot даже впоследствии выпустил свою реализацию.

Второй. Попытка «ответвиться» от оригинального веб-клиента Webogram. В нём была собственная реализация MTProto, однако, к тому моменту она не обновлялась уже несколько лет, сильно зависела от устаревшего AngularJS и не поддерживала веб-сокеты (вместо них — long polling). Тем не менее, по этому пути пошли сильные участники Merry Ant и Ace Monkey.

Третий. Библиотека TDLib, которая используется в нативных клиентах — написана на C++, в браузере запускается через WebAssembly и весит 9 МБ, что делает её недоступной в условиях медленного интернета. Кроме того, Safari каждый раз компилирует Wasm заново, из-за чего запуск приложения занимает до 10 секунд.

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

Четвёрый. Поиск open-source реализаций. Как оказалось, их существовало очень мало, и все они не работали. Некоторые поддерживали только Node.js, другие были заброшены, третьи просто не запускались. Довольно много времени ушло на безрезультатные попытки их реанимировать.

В итоге мне повезло. Я тогда каждый день заставлял себя читать группу @contests, где общались участники и зрители. Среди тысяч сообщений флуда там иногда проскакивала полезная информация, и однажды за пару дней до дедлайна кто-то выложил туда ссылку на GramJS — довольно свежую open-source реализацию, которая до этого совершенно никому не попадалась. Как и все остальные, она была абсолютно сырой и недокументированной, но буквально за несколько часов до окончания конкурса её всё-таки удалось запустить.

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

Продолжение следует.




Второй серьёзной задачей была реализация протокола шифрования и передачи данных.

Как известно, Telegram работает на своём собственном протоколе MTProto. Все данные передаются между клиентом и сервером в зашифрованном бинарном виде. Протокол содержит большое количество соглашений и алгоритмов, включая генерацию и проверку криптографических ключей с помощью Diffie–Hellman, сериализацию, шифрование с помощью AES-IGE, обфускацию, SHA256-хэширование и проверку подписей для всех отправляемых данных (включая медиа-файлы), а также многое другое.

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

Вот поверхностная схема протокола:


Вообще requestAnimationFrame используется в проекте повсеместно. Основная идея заключается в том, что если вы изменили что-либо в DOM, вы должны стараться избегать последующего считывания каких-либо свойств до следующего кадра анимации. Браузер старается производить все изменения и расчёты в конце кадра, поэтому в начале следующего кадра вы сможете воспользоваться уже рассчитанными значениями. В противном случае вы вызовете “forced layout”, который часто занимает значительное время и является причиной микрозадержек интерфейса.

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


Продолжение следует.


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

Снаружи Teact представляет из себя реализацию функционального подхода в React-е. С самого начала в нём нет ООП, зато есть полноценный механизм хуков, основанный всего на трёх самых главных: state, effects и memos. С помощью них реализуется всё многообразие остальных: как встроенных, так и кастомных.

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

На самом деле при первом рендере каждого компонента Teact создаёт для него «экземпляр», содержащий служебную информацию и хранилище для хуков. Этот экземпляр находится в иерархии виртуального DOM, поэтому всегда соответствует одному конкретному узлу. Хуки же управляются внутри каждого экземпляра с помощью перемещения курсора, иначе говоря просто обрабатываются по порядку. Именно поэтому так важно, чтобы порядок вызова хуков в компоненте никогда не менялся.

Хук useState в Teact реализован с учётом оптимизации, которой нет в React: после изменения состояния компонент будет отрисован только в следующем кадре анимации (при этом будет применено последнее заданное значение). Такой подход значительно уменьшает количество лишних перерисовок и позволяет избежать тяжеловесных операций layout в браузере.


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

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

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

Наиболее простым способом было бы подготовить всю необходимую разметку в HTML-файле и при необходимости создавать дополнительные ноды с помощью document.createElement. Последний способ, кстати, является более быстрым по сравнению с innerHTML. Однако, такой подход плохо масштабируется и приводит к быстрому разрастанию кода.

В тот момент пришла идея использовать JSX и Babel для его парсинга — препроцессоры не были запрещены условиями. Как известно, Babel превращает JSX-теги в вызовы React.createElement. Оставалось только создать глобальный объект с именем React и реализовать внутри него метод createElement, который создаст DOM-элементы и вставит друг в друга через appendChild.

Такой вариант отлично работал, но почти сразу пришло понимание того, что хорошим дополнением к нему стал бы виртуальный DOM. Этот подход позволяет хранить предыдущий результат парсинга шаблона и сравнивать его с текущим. Сравнение происходит очень быстро прямо в памяти JavaScript, после чего только необходимые изменения выборочно применяются к настоящему DOM-дереву.

Так родился внутренний фреймворк, который получил название Teact.


Около двух лет назад закончился первый JavaScript-конкурс, проводимый командой Telegram. Тогда я принял в нём участие, но смог занять лишь 2-е место. Однако, именно это положило начало событиям, в результате которых сегодня приложение Telegram WebZ стало одним из официальных клиентов Telegram.

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

Я убеждён, что сегодняшние веб-клиенты Telegram являются одними из самых сложных и технологичных среди существующих веб-приложений. В них сочетаются все самые современные браузерные API, технологии и методики. Сложные CSS-анимации, веб-воркеры и WebAssembly, PWA и многоуровневое кэширование, звукозапись и потоковая передача медиа, криптография и работа с двоичными данными, прогрессивные и оптимистичные интерфейсы, реактивные потоки данных и многое другое.

Постараюсь рассказывать обо всём понемногу и давать ссылки на конкретные примеры кода в репозитории Ajaxy/telegram-webz и на различные полезные ресурсы.

Оставайтесь на связи.





Показано 12 последних публикаций.

63

подписчиков
Статистика канала