Однажды вечером, попивая кофеек, я получил сообщение от коллеги с емким словом «Дожили» и ссылкой на выступление на PiterJS. В этом выступлении спикер взял сайт «Леруа Мерлен» и показывал, как надо делать оптимизацию на неоптимизированном сайте. И он прав. Если взять PageSpeed Insights, то на главной странице мы имеем следующие значения:
Мобилка | Десктоп |
Можно ли «пробить дно»? Да! И это зависит от того, в каком «настроении» находится PageSpeed Insights или в каком положении сегодня звезды на небе.
Я знаю, что техническая реализация нашего сайта проседает по всем пунктам. Но, учитывая, что я фронтлид проекта, меня это немного зацепило, ведь наш путь непрост, сайт сейчас находится в переделке, а в интернете кто-то неправ!
Первой системой по управлению сайтом «Леруа Мерлен» был Битрикс. С течением времени сайт вырос, и было принято решение подобрать систему покрупнее, а часть разработки перевести из аутсорса под свое крыло.
Новый проект получил название «Эльбрус», он должен был решить все потребности в построении омниканального бизнеса. Для front-end-разработки выбран Adobe Experience Manager (AEM). Был долгий и мучительный выбор, сравнения разных решений. В итоге тогда это решение показалось наиболее подходящим под наши требования.
Данная CMS довольно известна в Европе, США и Австралии. Но есть несколько проблем.
- В России ее почти никто не использует, поэтому разработчиков на эту систему найти сложно. Но ребята из подрядчиков не унывали и начали создавать сайт не на AEM, а на Java, используя AEM как виртуальную машину для своего кода.
- Сайт на Битрикс не прекратил свою жизнь. Он все так же рос и развивался, обрастал функционалом. Это породило состояние гонки, когда нужно было построить платформу и сайт, при этом постоянно пытаясь догнать свой оригинал.
- Изначально сайт на Битрикс был разделен на мобильную и десктоп-версию. HTML и CSS переносили like for like, почти copy-paste. На AEM было решено объединить все в адаптивном дизайне. Объединение двух изначально несовместимых структур HTML и CSS было проблематично и порождало множество багов и сложностей.
На тот момент у нас на сайте был следующий стек:
- бек, который умеет отдавать HTML;
- ES5, jQuery (и его плагины), underscore, less, bootstrap.
Для оптимизации фронта и отказа от легаси мы выбрали веб-компоненты. Не react/angular/vue/svelte, а веб-компоненты. Смиритесь с этим.
Для всех тех, кто скажет: «А почему не <мой любимый фреймворк>?» — есть следующие доводы:
- на тот момент бек не мог отдавать JSON, только HTML;
- даже если мы имеем возможность отдавать JSON, то у нас нет готовой платформы для SSR и налаженного общения с нашей CMS.
Но основная причина — необходимость «мягкой интеграции». Мне нравится это словосочетание, оно как бы говорит: «Возможно, такая реализация — это R&D, но сайт мы не сломаем, а профит получим». И это важно. Ведь наш продукт живет, он постоянно меняется, на нем добавляется или убывает функциональность. Через A/B мы тестируем новые подходы. И возможность мягкой интеграции, когда ты не ломаешь все старое, не переписываешь все с нуля, а постепенно пробуешь, оправдана и несет немало пользы. Мой босс любит повторять: «Ошибки надо совершать быстро», — чтобы иметь возможность потерять меньшее количество ресурсов на неуспешный вариант, сделать шаг назад и пересмотреть подход в соответствии с полученным опытом.
До того, как появился первый веб-компонент на нашем сайте, я примерно полгода пускал слюни на этот подход. Мне это казалось логичным развитием веб-технологий. А наличие полифилла в виде polymer как вынужденная необходимость, т. к. стандарты в браузере внедряются неторопливо. Тогда я несильно вдавался в подробности проблем с HTML import или v0 — v1. Но в тот момент, когда встал вопрос об оптимизации фронта, я решил попробовать именно их.
Для доказательства теории о том, что данный подход дает профит во фронте без серьезных изменений на беке, я попытался сделать реализацию продуктовой выдачи на веб-компонентах.
Реализация на vanila доставила достаточное количество боли. На тот момент я не нашел инструментов для удобной работы с веб-компонентами. Реализация рендера через innerHTML вводила в когнитивный диссонанс, а использование document.createElement создавало много лишнего кода.
Тогда же я нашел усеченный polymer в виде lit-element, находящийся в состоянии бета-версии. Он и lit-html показали, что веб-компоненты можно готовить удобно, но оставался вопрос по подключению компонентов на страницу, а также по отдельному подключению полифиллов для веб-компонентов. Я получил рабочую страницу, но ее реализация, размер бандла и то, как регистрировались веб-компоненты, меня не устраивало.
Stencil — это прям как второе дыхание, как свежий воздух на том легаси, которое я пытался оптимизировать. Он также был в бета-версии, а под капотом имел TypeScript. Его киллер-фича — это loader и подготовленные компоненты для всех возможных вариаций браузеров, которыми пользовались наши пользователи. Но как достоинство, так и проблема — это TypeScript. К сожалению, на тот момент у нас не было сильного TS-разработчика, который смог бы лидировать разработку на этом языке, а вкидывать в команду TypeScript, чтобы они везде указывали тип any, как-то бессмысленно.
Получив опыт sencil, я попробовал реализовать lit-element таким образом, к тому же на тот момент он вышел в стабильной версии. Вместе с коллегой мы смогли интегрировать полученные веб-компоненты с нашей бековой системой. И это был успех. Lighthouse радостно показывал 100 из 100 баллов, а пользователь, благодаря новой системе загрузки скриптов, получал js только тех компонентов, что видел во вьюпорте своего браузера. Это позволило сократить трафик для пользователя на 1/3. Дополнительным бонусом мы получили валидацию HTML через W3C-валидатор с нулем ошибок.
Расскажу немного про загрузчик. Одна из болей нашего легаси-сайта — это работа через http/1.1. И, как оказалось, перевести его на http/2 не представлялось возможным из-за каких-то реализаций на диспатчерах. Собирать бандл не хотелось, чтобы не кормить пользователя лишними байтами, а чанковать хотелось, так же как stencil. Было принято решение, что «приложение» нашего фронта должно быть отдельно от наших систем и храниться, например, на CDN, у которого есть http/2. В принципе, это распространенное решение.
Импорт модулей осуществлялся динамически, через dynamic import. Алгоритм следующий (если в двух словах):
- пользователю скачивается загрузчик, подключенный в самом конце body;
- загрузчик получает все ноды страницы из body;
- из этих нод получаем массив тегов в строковом представлении;
- этот массив фильтруем на то, чтобы в названии содержался дефис (который обязательно должен присутствовать в пользовательских элементах);
- из массива делаем выборку уникальных названий тегов;
- через switch…case пробегаем по известным загрузчику тегам и выполняем динамический импорт нужного веб-компонента, который при загрузке сам себя зарегистрирует.
В дальнейшем мы добавили в реализацию Intersection Observer, который указывал загрузчику, что подгружать компонент надо только в тот момент, когда он приблизился к вьюпорту пользователя. Как я указал выше, это решение позволило сократить количество JS-трафика на 1/3. Банальный пример — это футер сайта, который большинство пользователей не увидит за время сессии на десктопе, а пользователи на мобилке, по идее, скажут спасибо за отсутствие лишнего трафика.
Ну, и сборка осуществлялась через webpack как в es5, так и в es6. Спасибо open-wc за то, что предоставили генератор, который снял с нас нагрузку для настройки webpack.
Т. к. эксперимент с веб-компонентами был успешен в рамках исследования, мы решили не торопиться и не перепиливать ответственный функционал на новую для нас технологию. Для выпуска в прод мы с техлидом решили попробовать мелочевку, то, что большинство пользователей могут не заметить, что не повлияет ни на SEO, ни на путь пользователя.
Для нас такой сакральной жертвой стал вот этот элемент на продуктовой странице:
Пользователю вместо большого куска html приходила всего одна строка:
<uc-payment-options text="Способы оплаты" credit="visa mastercard mir" cash></uc-payment-options>
В последующем она наполнялась из JS в следующее:
К сожалению, пока я писал статью, этот элемент был заменен в рамках редизайна. Если интересно посмотреть на элемент, то добавьте эту строчку в HTML любой страницы нашего сайта через devtools.
Учитывая, что JS кешировался у пользователя, мы в итоге отдавали пользователю на каждой странице чуть меньше html (кстати, количество нод на странице — это еще одна проблема нашего сайта).
При переводе в продакшен данного компонента мы поймали только одну проблему — это CORS, который забыли настроить для данного CDN.
Для более серьезного шага в новую технологию пришлось поднимать вопрос о пересмотре политики поддерживаемых браузеров.
К сожалению, те полифиллы, на которые полагался lit-element и которые реализуют нечто похожее на shadowdom для старых браузеров — нестабильны. Там можно было получить все что угодно, но не то, на что рассчитывал. Это большая проблема рантайм-преобразований. В stencil сделали верно, они отказались эмулировать shadowdom, а на моменте сборки преобразовывали shadowdom в обычный набор стилей, спрятанный за уникальным неймингом классов. Поэтому в stencil еще можно добиться предсказуемой разработки компонентов с поддержкой ie11 и Edge, а вот в lit-element это отдельный сорт боли.
Дополнительную боль добавляло то, что тестирование не покрывало эти браузеры, хоть все и говорили, что мы их поддерживаем.
В итоге коллективным решением и подтверждением со стороны product owner, с рассмотрением статистики, а также трудозатрат на разработку, поддержание и тестирование решений для «старых» браузеров было принято:
- ie11 не поддерживаем.
- Основной браузер — Chrome и последние его 2 стабильные версии. Его мы «вылизываем» по максимуму, и покрытие тестами также в основном на нем.
- «Вечнозеленые» браузеры поддерживаем последние 2 версии, но баги на них правим с меньшим приоритетом.
- Edge (до 18 версии включительно) поддерживаем по остаточному принципу в связи с тем, что произошел релиз Edge 79 на chromium и он по умолчанию покрыт поддержкой.
Это важный и смелый шаг. Первое время мы в логах еще отмечали, что есть ошибки в неподдерживаемых браузерах. В итоге стали их игнорировать, т. к. отказ от них был незаметен ни для нашего трафика, ни для товарооборота.
И как итог — я могу не бояться выключить сборку в es5 и отказаться от полифиллов для него.
Следующим шагом было решение по архитектуре проекта с использованием веб-компонентов.
Изначально мы планировали разделить всю работу над фронтом на 2 части:
- ui-kit;
- бизнес-логика (elbrus-ui).
Скажу сразу, опыта построения ui-kit у меня не было. Возможно, из-за этого мы получили то, что получили.
Просчет был изначально: согласованная дизайн-система в компании отсутствовала, и шанс получить ее в ближайшее время — мал. Мы рисовали схемы о том, как будет удобно создавать компоненты на основе дизайн-системы, тестировать и смотреть это в storybook, а ради экспериментов, как конструктор, собирать в каком-нибудь HTML-конструкторе.
А в итоге разработка компонентов шла от дизайна страниц, которые в проде. И это было не самое разумное решение, т. к. сайт и так представлял собой набор компромиссов и костылей, а мы тут решили по-быстрому повторить негативный опыт, уже полученный при переходе с Битрикс на AEM.
Второй просчет — это попытка реализовать компоненты в виде готовых виджетов и прокидывать почти все, что нужно компоненту, в его атрибуты. А далее бизнес-логика как-либо расширяла бы эти компоненты. Оно так не взлетело — было сложно следить за компонентом сразу в 2 репозиториях и строить над веб-компонентом какую-либо сложную бизнес-логику, а при нахождении бага в верстке приходилось делать релиз, как ui-kit, так и elbrus-ui.
В итоге мы отказались от такого разделения до момента, когда дизайн-система не встанет во главу угла.
С бизнес-логикой тоже пришлось повозиться. Учитывая, что мы шли путем мягких интеграций, мы не могли взять и построить отдельный способ взаимодействия со сторами, который бы игнорировал предыдущую реализацию.
Мы пошли путем событийной модели и «слоев» бизнес-логики.
Каждый слой (basket, wishlist, compare, lk, analytics) создавал слушателей на document.body и слушал определенные события. Внутри они реализовывали работу со старыми сторами, чтобы не нарушать консистентность приложения, но при этом не иметь жесткой связки на то, какие компоненты сейчас присутствуют на странице (наш бек предполагает, что из режима автора можно управлять тем, какие компоненты будут на странице в html, поэтому нужна была слабая связанность).
Компоненты пушили custom event, те всплывали и перехватывались заинтересованным слоем.
Далее слой обрабатывал это событие и данные в нем, после чего выкидывал событие, например с именем basket:update.
Все компоненты, заинтересованные в поддержании состояния в зависимости от текущей корзины, проверяли, нужно ли обновить состояние своих элементов.
На мой взгляд, это напоминает redux, но более применима к тем задачам, которые стояли перед нашей командой.
Ну и, наверное, «мякотка», ради которой вы читали эту статью, — сами страницы на веб-компонентах.
Сразу предупрежу, что мы все еще в состоянии перехода с одного стека технологий на другой (а со стороны подступает еще и третий). Поэтому ссылки, которые я дам, могут содержать страницу на старом стеке + веб-компоненты, чисто на веб-компонентах, находиться в A/B и быть не такими доступными (но, я думаю, все мы умеем работать с куками в браузере).
Basket + checkout. Это первая серьезная переработка, которую мы сделали.
За счет того, что компоненты на lit-element реактивны и прекрасно работают со стейтом, мы попытались сделать реактивное приложение, но без реализации SPA (причины больше бизнеспроцессные и идеологические, а не технологические). Пользователю приходит страница с тегом uc-basket, который реализует бизнес-логику, формирует стейт и на его основе рендерит страницу. Тут мы не взяли redux/mobx, т. к. решили, что те возможности, что предоставляет lit-element, и те слои, что реализуют бизнес-логику, достаточны. Возможно, это и неправильно, потому что мы так же, как и со слоями, обменивались событиями вместо целенаправленного изменения единого стейта. Но это решение взлетело, хоть и не без багов. Мы почти полностью изолировали зависимость транзакционной части от нашего легаси-сайта. А за счет отделения ядра бизнес-логики от рендера редизайнить стало проще.
Header. Эту реализацию виджета с бизнес-логикой можно будет использовать на любом фронте, который поддержит его api.
SEO-чувствительная страница. Каких либо серьезных проблем, связанных с влиянием веб-компонентов на SEO, не было выявлено. Мы реализовывали страницу в соответствии с рекомендациями Yandex: всю SEO-важную разметку отдавать в lightdom. А то, что работает в Yandex, будет работать и в Google.
Главная страница (ушла в A/B с 23.12.19)
Последнее наше крупное изменение. Это компромисс между тем, что мы хотели, и тем, что мы можем предоставить пользователю, как в плане UX/UI, так и в плане технического исполнения. Содержит все наработки, указанные выше. Сама страница — это набор виджетов на веб-компонентах. С бека приходит HTML, состоящий только из пользовательских элементов, которые у пользователя гидрируются в браузере. Баги, что были получены, были выявлены в safari.
Приведу результаты PageSpeed Insights для пустой корзины.
Было | ||
Стало | ||
Мобилка | Десктоп |
Но, нет тех обещанных 100 баллов из 100.
Одна из причин: из 954kb (gzip) на странице только 120kb — это наши веб-компоненты (JS + CSS + SVG). Все остальное — это обслуживающие сайт скрипты, метрики и партнерские скрипты. Есть еще пара моментов, которые можно отметить в рекомендациях от PageSpeed Insights, но сильную присадку на мобилке нам дают сторонние скрипты.
Тут я бессилен, т. к. это метрики для бизнеса, и все сложнее, чем отказ от старых браузеров.
Сбор аналитики на нашем сайте также был болью. Раньше у нас был switch...case на 2500 строк, который пытался решить все кейсы и проблемы. А учитывая, что этот скрипт, который пушил в ga-контейнер, был в нашей сборке, какие-либо расширения или правки багов также привязывались к нашему релизному циклу.
Для сайта, который работает со слоями бизнес-логики, мы также решили, что будем работать с событиями.
Одна из мыслей о том, как наполнять это событие, чтобы соответствовать ТЗ, была следующей: всплытие этого события по DOM-дереву, обогащение его на каждом заинтересованном слое и отправка выше по дереву. Но такую систему сразу же отбросили, т. к. уж сильно она зависела от контекста элемента, который первый выкидывает событие.
И тогда мы пошли на пересмотр самого подхода для сбора информации в аналитику. При том же клике компонент, который обрабатывает этот клик, что-то знает о своем состоянии. Вот только это состояние он и выкидывает как событие, его перехватывает ga на уровне body. А дальше лишь чёрный ящик. Что происходит с данным событием, разработчики интерфейса не знают. И это правильно, наши бековые системы знают всю нужную для ga информацию. Мы смогли исключить аналитику из релизного цикла и тем самым сократить время на правки и внедрение новых полей в аналитику.
Но я бы соврал, если бы сказал, что это все проблемы, которые мы встретили, и что в итоге наступило светлое будущее и процветает социализм.
- Отсутствие доверия к технологии. Наверное, сложно доверять тому, чем почти никто не пользуется. Cейчас есть примеры того, как тот же Apple запустил свой музыкальный сервис с UI, построенным на веб-компонентах (stencil). Firefox, построил интерфейс служебной страницы about:config на веб-компонентах.
- Отсутствие интереса у сообщества. Я бы сказал, что есть энтузиасты, кто пробует и что-то реализует на веб-компонентах. И есть конторы типа Adobe или Oracle, кто пробует внедрять веб-компоненты в свои фронты. Но как я вижу отношение сообщества к данной технологии:
- Отсутствие разработчиков. Это связано со сказанным выше. Данный вопрос внутри «Леруа Мерлен» я решал через проведение воркшопов, написание статей (одна ушла на Хабр), доклад. Но получить разработчика со стороны, который будет готов погрузиться в «не react» — сложно.
- Отсутствие у меня опыта разработки ui-kit. Возможно, мне надо было «переболеть детскими болезнями», чтобы начать адекватно оценивать веб-компоненты и их место в наших UI, а также api, которое они должны реализовывать.
- Команде тестирования пришлось несладко, когда она познакомилась с shadowdom и его изоляцией.
- Подружили драйвер safari для selenium с shadowdom только через жуткие костыли.
- У веб-компонентов проблема с доступностью. Это прям большая проблема, связана она с использованием shadowdom. Как пример: с помощью клавиши таба в safari не попасть фокусом на кнопку, когда она лежит в shadowdom. Если вам критически нужна доступность, то веб-компоненты — это вообще не ваш вариант.
- Возможно, реактивность не нужна. Если мы рассматриваем веб-компоненты как атомарные элементы или лейауты, то реактивность там будет оверхедом, жрущим ресурсы и не дающим ничего, кроме единого подхода к реализации шаблонизации. Поэтому, если вы не реализуете виджеты с изолированной логикой, то использование того же lit-html, вероятно, избыточно.
- Возможно, не нужен constructible style sheets — еще один способ работы со стилизацией компонентов. По идее он дает профит в скорости рендера компонента за счет того, что ему уже определен набор стилей. Нет необходимости рендерить компонент и внутри него стили, которые потом парсить, и снова перерендеривать компонент. Идея хорошая, но реализована только в chrome, а для остальных браузеров необходимо использовать или полифилл, или готовые обертки типа lit-element. Лично для меня это еще один кусок JS, который не факт, что приносит на страницу профит по скорости отрисовки, но зато увеличивает рантайм JS.
Возможно, не нужен shadowdom. Только для изоляции стилей. Тут сильно холиварили с коллегой. Основная мысль в том, что shadowdom нормально не полифиллится, что, используя его, ты теряешь доступность. Slot, накладывает дополнительные ограничения на структуру компонента, позволяя стилизовать только первый уровень вложенного в него компонента. А ::part() не поддерживается всеми. Проблема, с которой я встретился, это попытка реализовать Яндекс-карту в веб-компоненте. Оказалось, что она использует глобальную область видимости, а потому, если помещаешь ее в shadowdom, будет баг. Вот тут статья о самом баге и о том, как я решил этот вопрос.
Еще одним неприятным моментом стало то, что атрибут loading=”lazy” для img не сработал из shadowdom. Это мой просчет, и данную проблему буду изучать, но пока надежнее data-src + IntersectionObserver не придумано.
Возможно, не нужна ленивая подгрузка, которую мы реализовали. Но тут, наверное, уже мои тараканы. Наверное это больше похоже на экономию на спичках, что это все никому не нужно, а пользователю что загрузить 116kb, что 380kb — роли не сыграет. Я провел поверхностный тест отказа от ленивой подгрузки и проверил результаты метрики performance в lighthouse с симуляцией медленного 4g в мобильном представлении.
max 47 21 min 19 2 lazy static
Согласен, что метрики не показательны.
Однако для Safari мы были вынуждены отказаться от ленивой загрузки из-за бага: если пользователь на мобильном устройстве резко скроллит страницу вниз, то видит только белый экран, а элементы начинают подгружаться после того, как пользователь попробует еще раз проскроллить страницу.
Плюс ко всему в текущей реализации подгрузки был найден неприятный момент. Если пользователь возвращается на страницу по истории браузера, то попадет не в начало страницы, а на ее середину, где были отрендеренные компоненты. Но выше текущей позиции компоненты все так же не отрендеренны и занимают не тот размер, который рассчитываешь. Из-за этого пользователь увидит много скачков на экране при загрузке и во время скролла.
Возможно, не нужны веб-компоненты. Давайте смотреть правде в глаза: где вы (сегодня) получат максимальный профит от веб-компонентов? Это узкий спектр проблем.
- Легаси-проект, в который хотелось бы добавить новую функциональность, применяя метод мягкой интеграции.
- Если в компании зоопарк либ и для дизайн-системы нужен какой-либо агностичный инструмент.
- Вы — энтузиаст и хотите построить свой UI не как у всех.
- Ваша публикационная платформа умеет отдавать только html.
- Если нужна изоляция стилей, структуры, скриптов. Например, для микрофронтенда, где в каждом кусочке сайта крутиться свой UI-фреймворк.
Постепенно идет разработка новой платформы для работы с микрофронтендами и за основу взяли react. Часть компонентов переиспользуются из текущей разработки веб-компонентов. Проблем со связкой react + lit-element не выявили. Месяца через 2 можно будет посмотреть результат на проде.
ainu
Стоит понимать, что примерно с 2018 года выполнение даже всех рекомендаций, которые даёт PageSpeed (включая пресловутое «Устраните ресурсы, блокирующие отображение»), не гарантирует попадание даже в «зелёную зону», я уже молчу про 100/100.
У них формула есть, которая суммирует веса метрик (хорошо хоть они известны), и выводит общий балл. В основном это время — TTFB, прорисовка первого экрана, получение интерактивности, компилирования JS и так далее. Вебкомпоненты не особо то ускоряют эти метрики.