Pull to refresh
833.09
Яндекс
Как мы делаем Яндекс

«Реактивный» интерфейс. Лекция Артёма Белова на FrontTalks 2018

Reading time12 min
Views8.2K
В браузерном JavaScript интерфейсы стали предсказуемы. «Однопоточные», с транзакционным сценарием отрисовки: пустой экран — загрузка — интерфейс. Разработчик Артём Белов из компании Cxense с упором на закон Парето рассказал, как, потратив 20% времени, отрисовать приложение на 80% быстрее за счет приемов «реактивного дизайна» — еще не сформулированных, но уже используемых в продуктах с приоритетом на UX.


Но что касается более матерых парней, которые не вылезают из Webpack и заставляют страдать Webpack, а не наоборот? Они подключают плагины. Но поскольку мы в начале развития этой техники, экосистема нам предлагает плагин, который берет ваш HTML, генерирует на его основе HTML со встроенным CSS, дает QR-код для рекламы… Серьезно? После этого ты можешь взять и через “!important” перебить стили… Пожалуй, стоит задать вопрос, почему я все еще во фронтенде.

— Всем привет, я Артём Белов. В наши дни слово «реактивный» могут понять неправильно: есть большой процент людей, которые выучили React перед изучением JavaScript. Поэтому произносить такие однокоренные слова немного неоднозначно. Ну а для меня это до сих пор физика, закон сохранения импульса.

Начнем с проблемы. Это наш профессиональный перфекционизм. Ведь мы не показываем интерфейс до того, как он не «идеален» — по нашему мнению. И я не знаю, что выиграет в номинации на информативность: белый экран браузера или спиннер с надписью «Feel free to wait forever»; мне не ясно.



Но одно можно сказать точно: отрисовка, как и отклик приложения — не транзакция. Отображение интерфейса — не логическая единица, так что это нужно бить на порции.



И во фронтенде, в общем-то, мы помогаем себе, мы завираемся.



В основном, когда говорим про «время загрузки» нашего приложения, мы не говорим фразу полностью — «среднее время загрузки». Хотя не нужно быть семь пядей во лбу, чтобы вторую загрузку приложения сделать мгновенной, при помощи Service Worker.

И грустно, что Lighthouse поощряет то, что приложение может не подавать признаки жизни и только на последних двух-трех кадрах аудита говорить: «Я вообще-то отрисовываться собралось».



И получить под 100 баллов в тесте приложения на performance сейчас — не самое сложное. Google это заметил, спасибо.



И такое емкое понятие, как «Time to Interactive», было разбито на стадии, а именно: «First Interactive», «Time to Consistently Interactive» и «First Input Delay». Если с первыми двумя терминами при помощи Google Translate можно понять, как взаимодействовать и улучшать, то последний можно объяснить при помощи взгляда на main thread браузера.



Пример. Если сразу после загрузки интерфейса пользователь осуществляет клик по текстовому полю, то может произойти пауза, прежде чем текстовое поле отобразит курсор с возможностью ввода. Всё потому, что main thread был занят, а интерфейс на момент клика казался готовым к взаимодействию.



Все это сводится к тому, что метрики в Lighthouse имеют практически одинаковое время завершения — то время как они должны быть сбалансированы.



«First meaningful paint» должен быть максимально быстрым, «First contentful paint» должен быть за ним, а у «Time to Consistently Interactive» должна быть не такая большая дельта от белого экрана браузера до полной готовности интерфейса.

Разумеется, есть эксперты, которые скажут, что это достигается просто:

«Нужно разбить приложение по роутам в Webpack и сделать Server-side rendering. Да тут работы на полчаса!»

Но это не совсем так… Уважаемый эксперт не упоминает, что каждый новый роут происходит одно и то же — загрузка. И снова все метрики в Lighthous» имеют малую дельту между собой, ведь роут, как правило, весомый.

И иногда не работаешь в большой успешной компании, и Server-side-рендеринга там нет. И, пользуясь служебным положением, приступаешь к R&D с понятной целью — получить прогрессивный рендеринг. Как прогрессивный JPEG, что в 167 параграфе «Ководства» Лебедева.

В принципе, R&D-план выглядит очень просто… Нужно каким-то образом получить профит. И не забывать о главном — о времени и сроках, которые любят гореть. И, держа манифест минималиста-программиста в руках, ты должен помнить: трать 20% времени на 80% результата.

Я заручился инструментами. Например, lag-radar.



Правда, я не углублялся в Санта-Барбару, кто украл идею, Дэн Абрамов у кого-то или наоборот… На последней презентации JSConf Iceland 2018 была показана довольно интересная концепция: радар зеленый до того момента, пока не обнаруживает задержки в main tread, и когда они чувствительные — радар становится желтым, а когда критичные — красным.

И, нажав Cmd+Shift+P на Mac — или Ctrl+Shift+P на Windows, — я посмотрел на процент покрытия кода. А потом посмотрел на бандл.





И опять на покрытие, и опять на бандл. И попросту решил принять ситуацию. Мне не нужно ничего отсюда убирать. Ведь в проектах многие используют инструменты, которые являются стандартами де-факто. Например, Иван Акулов совместно с Google Chrome Labs давным-давно сделали репозиторий по оптимизации вебпака, webpack-libs-optimizations. И зачем мне это повторять, рассказывать про Сode Split, про Lazy load? Это то, что является известными и прогрессивными стандартами в 2018-м.



И отвечая на вопрос «А что можно загрузить за полсекунды?» — JS и тут победитель. Например, можно загрузить 100 килобайт JPG и потратить 0,04 на его обработку, но JS его уделывает, ведь на его обработку нужно гораздо больше времени, в 20 раз.



Потому что надо помнить, что происходит во время старта приложения.



После загрузки начинается та вещь, которую хочется докомпозировать: загрузить в один момент, а парсинг, компиляцию и исполнение кода произвести в другой момент времени.



Я посмотрел, что предлагает нам тег <script />. Вариантов много, и я подумал воспользоваться подсказкой.



Хм, в зале нет пультов, телефона нет. Беру 50:50. У нас остается варианты “defer” и type=”module”.



Они загружают, не блокируя парсинг, а обрабатывают себя и исполняются потом, когда приложение наиболее готово к этому — освобождение main thread.

Но тут нет правильного варианта, и вспоминаются слова великого человека Бена Шварца. Не то, чтобы он сказал это по-русски, но:



Чтобы его не было, мне нужен JS, поэтому Initial Request — это единственный метод того, как я могу избавиться от ненужных запросов.



Ну и придется настраивать Webpack в очередной раз. Не всегда же это подключение плагинов.



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



Наверное, их нужно вынести в Critical-запрос?



При помощи ConcatenationPlugin() очистим манифест Webpack, то есть список всех файлов модулей Webpack, после чего встроим этот JSON, тем самым избавиться от этого ненужного запроса. После чего определить, есть ли асинхронный код, который можно встроить. И, наконец, встроить мозги Webpack прямо в страницу, чтобы не ждать, пока все модули загрузятся. Да, взять чанк runtime и встроить в HTML.



В общем-то, встройка мозгов Webpack дает не самый фееричный прирост, но теперь поговорим о Critical CSS.



Но… мы же поняли, что Critical CSS для парней, у который single page application невозможен. Ведь без полного HTML роута, инструменты не смогут построить Critical CSS.



И хорошо, что есть Puppeteer, который смог.



Ведь то, что презентовали вместе с ним — папку “puppeteer-examples” на GitHub расхватали на проекты. Так и появился проект Rendertron. Правда, чтобы получить от него хоть какую-то ошибку, необходимо вставить адрес, нажать Enter… и всё.



Хорошо, что есть другие npm-пакеты, которые работают. Они на рынке, видимо, подольше.



После установки и билда пакета, обычным curl я могу на локальном сервере пререндерить HTML моего приложения, и получить тем самым Critical HTML, так нужный для создания Critical CSS.



Бутстрап HTML вместо ожидания рендера дал прирост, ожидаемо, но останавливаться на этом рано. Продолжим путь к Critical CSS.



В очередной раз спасибо Эдди Османи за то, что позаботился об экосистеме и создал HtmlCriticalWebpackPlugin(), где для конфигурации нужно ответить всего на пару вопросов: куда сохранять Critical CSS и в каком виде?



Оу, можно заметить подозрительную строку «painthouse». Это весьма интересный момент, когда Эдди Османи использует сторонний пакет, у которого есть, выясняется, даже веб-интерфейс.



Но если честно, мне стыдно пользоваться веб-интерфейсом, это, видимо, для верстальщиков, у которых командная строка не установлена. Но тут все просто, как в рекламе Юппи из 90-х: просто нажми «Create».



И это дает не самый фееричный прирост, но весьма чувствительный для первой прокраски приложения.



С миру по нитке, и приложение немного подходит к рубежу, когда шрифты начинают плясать. Этот рубеж известен как 3 секунды. И начинается раздел, в котором мы, фронтендеры, редко проигрываем, а именно — игра со шрифтами.

Рецепты по загрузке шрифтов. Здесь нам поможет font-display. Поняв, что тут нужно не двигаться сразу с места в карьер, сперва определимся: flash of invisible text, FOIT, или flash of unstyled text, FOUT, страшен для нас.



И — спойлер — наиболее страшным для нас является flash of invisible text, так как он вызывает большее количество перерисовок.



Если вкратце, посмотрев, какие существуют свойства font-display, правильно выбрать для себя swap или fallback. По большому счету, из пяти свойств вы можете подобрать нужное для себя, тут нет правильного варианта.



Более того, я слышал, что есть проект Font Style Matcher, который позволит мне для моего Comic Sans-шрифта подобрать приличный системный fallback-шрифт и не грузить его с Google Fonts. Хотя и это не совсем то…

Есть такие люди, как Зак Зезерман, которые выдумали концепцию FOFT.



Которая практически не видна в браузере.

Это достигается за счет того, что когда вы загружаете шрифт roman стандартного начертания, а браузер уже настолько умен, что может сделать «фальшивый» жирный, курсив, подчеркнутый, и зачеркнутый шрифт на основе стандартного начертания. Это дает вспышку, но практически никакую. Для этого нужно осуществить так называемый preload шрифта.



Но что если я скажу, что шрифт можно встроить в CSS? И это действительно так, и это тоже дает прирост!



Выглядит это как комбинация техник. Необходимо просто встроить roman шрифт прямо в CSS. Тем самым и получить прирост на первом главном знакомстве пользователя с приложением, а затем догрузить оставшиеся гарнитуры, когда это будет нужно, но не сразу.



И вот, спустя столько манипуляций, «Time to Consistently Interactive» по-прежнему имеет весомую дельту от остальных показателей. Я улучшил только прокраску приложения. Это неплохо, но не то, к чему я стремился. По-моему, мне надо поймать какую-то проблему. И она была поймана, причем за хвост. Ведь:



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

Но опять, эксперты подскажут — есть же lazy load.



Но вспомните, как вы с ним взаимодействуете. Вы не можете покрыть 100% случаев.



Да и если пойти по пути оптимизации и выполнить все чекпоинты в книге Эдди Османи «Essential Image Optimization», то тоже остаешься с нерешенными проблемами…

Но я увидел, что Эдди Османи любит упоминать в своих альманаках и статьях на Medium компанию Reddit. Я вспомнил, что у меня там работает знакомый и позвонив ему за советом, он напомнил мне, что есть такая вещь как Percieved performance, а это определение лежало в чертогах моего разума.



Тут нам, кстати, помогают дизайнеры. Это когда играет не время, а то, как ты преподносишь свое приложение. И весьма правильная тактика для этого — «content placeholders».



Например, это умеет делать YouTube.



YouTube умудряется проигрывать видео еще до того, как все вспомогательные компоненты были загружены. Осуществляется это, забегая вперед, за счет content placeholders, таких серых анимированных плашек.



А что нам предлагает экосистема на данный момент? Ничего особенного. CodePen. Посмотрим реализации у них.



Вполне себе карточка, имеющая место в любом паттерне дизайна. Посмотрим…



Нужно объявить немного переменных на 30 строк, объявить грамм свойств на 50 строк, немного анимаций и мы получим 100 строк кода… Это 10 из 10 по шкале Лиа Виру.

Но что касается более матерых парней, которые не вылезают из Webpack и заставляют страдать Webpack, а не наоборот? Они подключают плагины. Но поскольку мы в начале развития этой техники, экосистема нам предлагает плагин, который берет ваш HTML, генерирует на его основе HTML со встроенным CSS, дает QR-код для рекламы, видимо… Серьезно? После этого ты можешь взять и через “!important” перебить стили… Пожалуй, стоит задать вопрос, почему я все еще во фронтенде.



Но есть команда людей, для которых шутки про их продукт просто не смешны. PostCSS.



На них всегда можно положиться. В частности, можно реализовать данную технику — «contant placeholders» — на концепте CSS «multi background». Это когда одно свойство “background” может вмещать в себя через запятую несколько значений.



CanIUse как раз отвечает, да.



Это выглядит следующим, знакомым для нас образом. Знакомым — благодаря Medium.



На ваше изображение накладывается линейный градиент, который отображается максимально быстро, а над ним, позже, проявится полноразмерное изображение. В коде это выглядит примерно так.



Первым приоритетным запросом начинает грузиться изображение, разумеется, тяжелое. А мгновенно, при получении CSS, отобразится линейный градиент. Но linear градиент не настолько прямой и плавный, как easing.



Поэтому код следует подхватывать другим плагином, который сделает linear-градиент более плавным.



Но немного-то градиентом сделаешь наложений, чтобы они покрывали все случаи.



В этом случае это не отработает должным образом, так как изображение слишком сложное, для создания абстракции всего-то линейным градиентом. Но тут сработает sqip.



Это CLI-модуль, чтобы создать placeholder вашему изображению и прикрепить его при помощи техники «multiple backgrounds».



Разумеется, полноразмерное изображение будет грузиться намного дольше, а placeholder от sqip в силу своего веса отобразится гораздо быстрее.



Но это не совсем то, к чему я стремлюсь. Я бы хотел, чтобы тяжелые изображения приходили, когда мне нужно, а не когда этого хочет браузер. И мне нужно разбить это на два CSS-файла, поместить один в <head>, где будут лежать легкие изображения, и потом догрузить в <body> тяжелые изображения.



Но как быть с оставшимися изображениями, например, в JS?



Манипуляция с SVG, в частности, выглядит неутешительно. Ты вроде бы и хочешь перерисовать SVG, но через привычный CSS оно тебе не подконтрольно.



Видимо, согласно новым веяниям, SVG придется стать компонентом.



При помощи того пакета, который был принят Дэном Абрамовым в “create-react-app” — это “svgr”.



Это CLI инструмент, который попросту вставляет SVG в ваш JS, и вы можете с ним работать как с компонентом. Это три строчки кода, фактически.



Но дизайнеры, видимо, не в курсе, что у многих фреймворков должен быть один root-элемент компонента. Я же не попрошу его «по-особому» экспортировать SVG из Sketch?



Из-за ряда мелких подобных проблем приходится обращать внимание на подозрительно популярные проекты.



Например, какой-нибудь “react-icon-base”, который предлагает себя как замену тегу <SVG>. А именно, пакет как бы говорит нам: вместо тега <SVG> оберни в меня свой SVG-код, после чего я применю свойства: fill, text, stroke и все что угодно, сам, потому что во мне заложена логика, которая служит «мостом» из CSS.



И при помощи нехитрой команды можно сделать свой svgr-шаблон для своего проекта.





Если честно, я не знаю, почему этот проект только для React.

Далее вы можете импортировать иконки, которые не обрастают синтаксическим сахаром, каким бы вам ни было удобным образом.



И наконец-то отойти от устарелого, возможно, тега <img> в сторону компонентного подхода, о котором все говорят.



А в пользу чего? Например, в пользу того, чтобы вы попробовали применить псевдокласс “:export” в ваших SCSS\SASS файлах.



Я с большой вероятностью уверен, что это работает и в Webpack вашей версии. Псевдокласс “:export” выгружает переменные, которую вы объявите в SCSS. После чего вы можете импортировать их в JS коде. И всё, и не нужно обрастать велосипедами, как какой-нибудь JSON-файл со всеми цветами и прочим.



Svgr позволяет уменьшить первую прокраску приложения. Но не настолько честно, потому что код попросту мигрировал из CSS в JS.

Но секунду, мы же забыли про sqip, каков будет прирост после его применения?



Обладая нехитрыми навыками динамического импорта, применим следующую технику. Подгрузим легкую версию изображения, sqip placeholder, и отобразим ее в интерфейсе. Потом в динамическом импорте, в .then(), дождемся полноразмерную иконку, не блокируя взаимодействие с интерфейсом.



Это приводит к тому, что мы получаем подвластный нам SVG, который мы можем переопределить аргументами, как настоящий JS-компонент.



И наконец-то это дает прирост в нужных метриках «интерактива». И тем самым вы отвечаете на главный вопрос, который озвучен в статьях Эдди Османи: «А вообще что-то происходит?» Вы отвечаете на него гораздо раньше: «Да, происходит».



Но такое чувство, что у меня в приложении одни иконки. Есть же еще и поля форм, например.



Здесь есть решение, которое можно гуглить прямо сейчас. Например, “react-content-placeholder” и “vue-сontent-placeholder”. Но стоп, здесь нам предлагают рисовать SVG-плейсхолдеры компонентов вручную.



Есть и веб-версия, где вы можете через drag&drop перетаскивать и создавать плейсхолдеры. Которые как раз и будут похожи на ваши реальные компоненты.



Но то ли дело подгрузить styled-components пять раз — нет! — и styled-system, которыми я пользуюсь.



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



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

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



И если бы я был на Оскаре, то достал бы конверт и произнес бы что-то вроде: «За невероятную смекалку в реализации content placeholder, а именно в реализации анимации при помощи гифки 10х10 пикселей, вместо того чтобы писать CSS-стили, присуждается звание в области экспертов в анимации, с приветом из прошлого, Instagram!»



Надеюсь, они не делают отступы прозрачными однопиксельными гифками, а свойство border у них — не двойная таблица.

И я бы хотел вам напомнить: опирайтесь на real user monitoring, RUM.



На реальных пользователей ваших проектов и актуальные метрики. Возможно, стоит мерить не среднее время загрузки, а например, first input delay.

Есть вероятность, что в вашем приложении есть скелет, а не только common-часть, которую вы загружаете и вроде бы спокойны.

Я очень рад, что Эдди Османи вбил нам в голову то, что вес приложения и трудозатраты не равны друг к другу.

Не знаю, как кто относится к Лебедеву, но используйте метод прогрессивного JPG не только в жизни, но и при построении интерфейсов.

Управляйте загрузкой ресурсов, иначе за вас будет ей управлять браузер. Возможно, не всегда ожидаемо.



И последнее. Такое чувство, что через некоторое время, когда народ задумается, что нужно не компоненты грузить по роутам, а бить приложение на компоненты, вы наткнетесь на какой-нибудь loadable-пакет. Посмо́трите, что о нем написано, какие люди о нем говорят. Поймете, насколько это правильно — делить бандл не на роуты, а на компоненты. После чего пролистаете еще ниже — потому что это маркетинг, где темные части нужно прятать. Вы увидите, что, оказывается, это все одна грамотная обертка над динамическим импортом Webpack. Ну а дальше вы прочитаете инструкции, как создать… great loading component? Управлять загрузкой, если что-то пошло не так? Избегать вспышек при отложенной загрузке компонент?

Такое чувство, что я сейчас рассказал тот подход, который скоро, надеюсь, можно будет найти поиском. А вам спасибо за внимание.

Tags:
Hubs:
+18
Comments1

Articles

Information

Website
www.ya.ru
Registered
Founded
Employees
over 10,000 employees
Location
Россия