Каждая миллисекунда имеет значение, когда кто-то собирается вам заплатить. Попадая на страницу оплаты, клиент принимает решение за доли секунды — еще до того, как нажмет кнопку «Оформить заказ». Безопасно ли это? Настоящее ли это? Это впечатление начинается со скорости загрузки страницы. Пользователи начинают задаваться вопросом, защищен ли их платеж, существует ли бизнес, стоящий за этой страницей, и получат ли они то, за что заплатили.
Скорость — это не пустой показатель. Это первый сигнал доверия, который получает ваш клиент, а для платежных ссылок он, пожалуй, самый важный. Мы переработали механизм рендеринга платежных ссылок в Moov, и результаты говорят сами за себя:
На десктопе мы теперь достигаем FCP 0.6 с, Speed Index 0.6 с, LCP 0.9 с и CLS 0.019. Это практически мгновенно для полностью интерактивной платежной формы. Вот скриншот наших результатов в Lighthouse для десктопов.
Вот что именно мы изменили и почему.
Проблема
Наши платежные ссылки представляли собой SPA на React. Сервер возвращал минимальную оболочку index.html с полезной нагрузкой JSON, браузер загружал пакет JavaScript, React начинал рендеринг, данные запрашивались, и только после этого пользователь наконец видел платежную форму. Общий объем загрузки без кэширования составлял 4.2 МБ по сети.
Для страницы, единственная задача которой — принять платеж, такая архитектура работала против нас. Каждый байт и каждый сетевой запрос вставали между клиентом и кнопкой «Оплатить».
Отчет Portent показал, что у интернет-магазина со временем загрузки 1 секунда коэффициент конверсии в 5 раз выше, чем у сайта со временем загрузки 10 секунд. Это серьезный прирост.
Решение: серверный рендеринг с клиентской гидратацией
Мы перенесли рендеринг из браузера на сервер. Вместо отправки пустой оболочки и тяжелого JS-бандла мы теперь возвращаем полностью отрендеренный HTML из вспомогательного SSR-сервиса на базе Bun.
Поток запросов выглядит следующим образом:
- Запрос поступает на наш сервер
- Сервер извлекает данные платежной ссылки
- Вспомогательный сервис на базе Bun рендерит дерево компонентов React в HTML
- Готовая страница возвращается в первичном ответе
- Клиентский React выполняет гидратацию для обеспечения интерактивности
Браузер получает то, что можно отобразить немедленно, вместо того чтобы смотреть на пустую страницу во время загрузки, синтаксического анализа и выполнения JavaScript.
Почему именно Bun?
Мы оценили несколько подходов: SSR на базе Node, граничный (edge) рендеринг и статическую прегенерацию. Bun победил благодаря простоте и высокой производительности. Встроенная поддержка JSX, нативная поддержка TypeScript и быстрое время запуска сделали его естественным выбором для вспомогательного сервиса, которому необходимо рендерить компоненты React при каждом запросе без накладных расходов полноценной среды выполнения Node. При этом нам не потребовалось менять систему сборки или переписывать код. Он просто выполняет ту единственную задачу, которая нам нужна, делает это быстро и потребляет меньше памяти, чем Node.
Что помогло сдвинуть дело с мертвой точки
Миграция на SSR стала главным изменением, но в совокупности несколько факторов позволили нам сократить FCP на мобильных устройствах с 11 с до 2.2 с, а на десктопах — до 0.6 с:
- Устранение «водопада» рендеринга. В модели SPA браузер должен был загрузить JS → выполнить JS → запросить данные → отрендерить. С SSR получение данных и рендеринг происходят на сервере за один проход. Первый ответ браузера уже содержит готовую страницу.
- Меньший размер начального пакета. Сервер возвращает HTML, который браузер может отрисовать немедленно, а не приложение JavaScript. Пакет гидратации составляет лишь малую часть от исходного бандла SPA, поскольку ему нужно лишь привязать обработчики событий к существующему DOM, а не воссоздавать его с нуля.
- Более умное разделение на чанки и ленивая загрузка. У нас всегда были разделение на чанки, ленивая загрузка и удаление мертвого кода, но мы смогли улучшить это, более агрессивно разделив кодовую базу и загружая только тот код, который необходим для текущего маршрута. Например, нам удалось удалить код страницы выплат из бандла страницы оплаты и наоборот.
- Отсутствие смещения макета. Поскольку сервер рендерит финальный макет, страница не «прыгает» по мере монтирования компонентов и прибытия данных. Наш показатель CLS отражает это.
И, конечно же, дизайн страницы играет большую роль. Мы загружаем высококачественный логотип компании как можно быстрее, используя loading="eager" и decoding="async". Позиции в заказе и сумма платежа видны сразу, и единственное, что остается загрузить — это платежная форма, которой нужны подсказки от браузера (например, следует ли показывать Apple Pay).
Небольшое отступление об изображениях продуктов. Мы масштабируем изображения до 2x на экранах с высокой плотностью пикселей, а на основе добавленного количества и класса размера загружающего страницу браузера загружаем соответствующее количество изображений, обязательно откладывая загрузку тех из них, которые не находятся в поле зрения.
Небольшое отступление об изображениях продуктов. Мы масштабируем изображения до 2x на экранах с высокой плотностью пикселей, а на основе добавленного количества и класса размера загружающего страницу браузера загружаем соответствующее количество изображений, обязательно откладывая загрузку тех из них, которые не находятся в поле зрения.
Обратная сторона
SSR не дается бесплатно. Теперь мы запускаем вспомогательный сервис, который должен работать стабильно и быстро. Если SSR-сервис работает медленно, каждая загрузка страницы замедляется, поскольку нет клиентского фоллбека, скрывающего задержку. Чтобы сервис работал быстро, мы запускаем бэкенд-сервис на Go и контейнеры SSR на Bun в одних и тех же подах Kubernetes, сокращая сетевые накладные расходы. SSR-сервис отвечает только за прием данных от бэкенд-сервиса для создания дерева компонентов React и предоставление статических ассетов.
Результаты
Показатели на мобильных устройствах говорят сами за себя. Мы сократили First Contentful Paint на 80%, а LCP — на 74%. На десктопе FCP упал до 0.6 секунды. Это настолько быстро, что создается ощущение, будто страница уже была загружена.
В частности, для платежных ссылок скорость — это не приятная опция, а разница между завершенной транзакцией и брошенной. Если ваш процесс оформления заказа на телефоне загружается 16 секунд, какая-то часть этих клиентов уходит.
Мы верим, что программное обеспечение должно быть красивым и быстрым. Мы еще не закончили, но гордимся достигнутым прогрессом.
Хотите создавать продукты вместе с нами? Присоединяйтесь к нашей команде.
