Около года назад вышла статья-манифест Никиты Прокопова о разочаровании в программном обеспечении. Судя по положительным откликам, разработчикам небезразлично качество производимых продуктов. Может быть пора начать действовать?
В этой заметке я хочу рассказать о своей разработке, которая, по моему мнению, может вылечить основные проблемы производительности современного веба и сделать пользователя немного счастливее. Проблемы такие: большой вес JS-кода, высокое время до начала работы со страницей (TTI), высокое потребление памяти и процессора.
Прежде чем читать дальше, перейдите по ссылке. Попробуйте поиграть пару партий. Играть желательно с десктопа.
Немного истории
С самого начала веб-браузер был задуман как тонкий клиент для веб-серверов. Браузер отображал гипертекстовые страницы, которые получал от сервера. Просто и изящно. Как это часто бывает, красивая идея столкнулась с реальностью, и уже через несколько лет производители браузеров добавили поддержку скриптового языка. Сначала он служил лишь для украшательства и вплоть до середины нулевых хорошим тоном считалось делать веб-страницы работающими без поддержки JS.
Современный подход к разработке веб-сайтов это результат развития требований к интерактивности пользовательского интерфейса. Задачи по улучшению интерактивности легли на плечи верстальщиков. Те зачастую не имели компетенций и полномочий разрабатывать "сквозное" решение. Верстальщики научились писать JS и превратились во фронтендеров. Логика постепенно начала перетекать с сервера на клиент. Фронтендеру удобно писать для браузера, а бекендкеру удобно не думать о пользователе (мое дело тебе JSON отдать, а дальше хоть трава не расти). Буквально два года назад можно было наблюдать всплеск интереса к serverless-архитектуре, где предлагалось что JS-приложения будут напрямую работать с БД и шинами событий.
На данный момент "сферический веб-сайт в вакууме" представляет собой сложное приложение на JS и простой API-сервер с которым оно общается. Основная логика выполняется на толстом клиенте, серверная часть вырождается до простой прослойки к БД.
Необходимость держать логику на клиенте порождает проблемы. Если сервис "взлетел" и стал зарабатывать, то дальше, в плане производительности, ему будет только хуже. Будут меняться требования. Будет меняться команда разработки. Нового кода будет все больше, а старый код не будет вычищаться. Страница распухнет от зависимостей, будет загружать JSON о назначении которого уже никто не помнит, но удалять страшно, вдруг что-то сломается. Будут появляться фоновые задачи setInterval
, каждая из которых выполняется по несколько миллисекунд каждую секунду, что через какое-то время приведет к тормозам и разогреву айпада несчастного пользователя до того, что он сможет жарить на нем яичницу. Закончится это тем, что выгоревшие фронтендеры придут к менеджеру с предложением переписать все с нуля на новом фреймворке. Менеджер откажет, и фронтендеры станут использовать два фреймворка совместно.
Как работает Королев
Так вот, что если вернуться к точке отчета? Тому моменту, когда кому-то пришла в голову идея обновлять контент без перезагрузки страницы, и историческая неизбежность породила AJAX? Что если оставить все на сервере и сделать тонкий клиент? Лучшие сайты делают пререндеринг страниц на сервере (SSR), чтобы пользователь видел интерфейс до того как загрузится и запустится JS. Можно пойти дальше и оставить на клиенте только код, который отвечает за обработку ввода-вывода учитывая современные требования к интерактивности. Мысли об этом вылились в проект "Королев".
Как это работает со стороны клиента? Пользователь приходит на страницу. Сервер отдает сформированный HTML и небольшой скрипт (~6 Кбайт без сжатия), который соединяется с сервером по веб-сокету. Когда пользователь производит событие (например клик), скрипт отправляет его на сервер. Обработав событие, сервер формирует список команд вида "добавь новый <div>
туда-то", "добавь класс к такому-то элементу", "удали такой-то элемент". Клиент применяет список команд к DOM. Как таковой работы с HTML не происходит — скрипт работает непосредственно с DOM, поэтому не стоит беспокоиться что содержимое формы или позиция скролла будет сброшена.
Что происходит на сервере? Когда от браузера приходит запрос на страницу, Королев создает новую сессию. Для сессии производится начальное состояние, которое сохраняется в кеше. Из этого состояния формируется HTML, который отдается клиенту в качестве ответа на запрос. Кроме этого сервер сохраняет в сессии "виртуальный DOM". После запроса страницы, сервер принимает запрос на открытие веб-сокета. Королев связывает открытый веб-сокет с сессией. Каждое событие приходящее с клиента может изменить состояние связанное с сессией. Каждое изменение состояния приводит к вызову функции render
, которая формирует новый "виртуальный DOM", который сравнивается со старой версией. В результате сравнения получается список команд для отправки на клиент.
Что происходит в коде и голове разработчика? Описанное выше могло напомнить вам React, с тем отличием что все выполняется на сервере. С точки зрения подхода к разработке мы так-же имеем что-то подобное. Поэтому, если вы работали с React или другим "виртуальным DOM", то подход Королева будет вам знаком. Если вы не знакомы с React, то представьте что у вас есть данные которые вы вставляете в шаблон. Представьте, что по шаблону раскиданы обработчики событий которые умеют изменять данные (но не DOM). Вы изменяете данные, страница изменяется сама. Королев сам придумывает как изменить DOM.
Производительность
Есть два популярных вопроса по поводу Королева: "что если задержки высокие" и "не нагрузит ли это мой сервер". Оба вопроса весьма резонные. Фронтенд-программист привык что его программа работает на локальной машине пользователя. Это значит что изменения произведенные ей применятся как только JS-машина закончит выполнение кода и браузер начнет рендеренг. Я специально показал пример использования "на максималках" в самом начале. Вы могли наблюдать задержку только если заходили с другой стороны земного шара (сервера расположены в Москве) или сидели в интернете через GPRS. Ну или мой жалкий виртуальный сервак с одним ядром и 1 Гбайт оперативки не выдержал хабра-эффекта.
Вопрос про нагрузку на сервер задают обычно бекендеры. Движок вывода изменений работает очень быстро: ~10 тыс. выводов в секунду для двух произвольных деревьев из 500 узлов на младшем макбуке 2013 года. Статический рендеринг тоже дает довольно хороший результат: до 1 млн. страниц в секунду. Каждый "виртуальный DOM" хранится и обрабатывается в специальном сереализованном виде и занимает 128 Кбайт для средней веб-страницы. Процесс вывода специально оптимизирован и не имеет оверхеда по памяти и GC.
Что касается скорости разработки, то здесь Королев дает большие преимущества. Не нужно писать дополнительную прослойку между БД и сервером. Не нужно согласовывать протокол между клиентом и сервером. Не нужно заботиться о модульности фронтенда — вес JS на клиенте всегда останется одинаковым. Не нужно делать дополнительную логику для серверных событий: просто примите сообщение из очереди и измените состояние сессии, Королев отрендерит и доставит.
Цена
За преимущества придется заплатить. От каких-то привычек придется отказаться, а какие-то новые приобрести. На пример придется отказаться от JS-анимаций и удовлетвориться CSS-анимациями. Придется научиться делать инфраструктуру изначально геораспределенной, если хотите обслуживать пользователей из разных стран качественно. Придется отказаться от JS и перейти на Scala.
Мне немного стыдно (на самом деле нет), что я ввел читателя в заблуждение и не сказал сразу что Королев написан на Scala. Дочитали бы вы до этого момента, если бы я сразу рассказал об этом? Рассказывая о Королеве мне приходится преодолевать два стереотипа. Первый связан с тем что серверный рендеринг воспринимается как что-то медленное, не интерактивное. Второй связан с тем что Scala это что-то сложное. И первый и второй стереотип не имеют к реальности никакого отношения. Более того программировать в стиле React на Scala удобнее чем на JS. Современный JS тяготеет к функциональному программированию, Scala дает его из коробки. На пример, у любого объекта в Scala есть метод copy()
, который позволяет скопировать объект изменив некоторые поля. Иммутабельные коллекции встроены в стандартную библотеку Scala.
Заключение
Королев разрабатывается уже три года несколькими разработчиками и многие "детские" проблемы в нем решены. Проект хорошо документирован, вся функциональность покрыта примерами. Предлагаю начинать внедрение Королева с небольших самостоятельных сервисов. Я надеюсь, что Королев поможет сделать программы менее разочаровывающими.