Простые правила при работе с растровыми изображениями на каждый день / Хабр | Веб-студия Nat.od.ua
Простые правила при работе с растровыми изображениями на каждый день / Хабр
Привет, меня зовут Денис, я руковожу направлением разработки в Домклике. Дополнительно несу ношу лидера frontend-направления в нашей компании. Не так давно я отрефакторил систему собеседований для frontend-разработчиков, попутно тестируя технические вопросы на внешних и внутренних респондентах. И пришёл к выводу, что множество мной опрошенных разработчиков, вне зависимости от уровня, не знают или просто не обращают внимание на базовые правила при работе с картинками. В результате на просторах интернета зачастую можно найти изображения размером 200 на 200 пикселей и весом в несколько мегабайтов со смещением макета, столь раздражающим пользователей. Если вам интересно, как практически без вложений улучшить пользовательский опыт, то прошу под кат.
Содержание:
Исходные данные и выбор основного формата
Изменение размеров
Оптимизация
WebP
Тег picture и srcset
Предзагрузка
Ленивая загрузка
Сдвиг макета
Object-fit
Исходные данные и выбор основного формата
Для того, чтобы статья была наглядной и последовательной, я взял из одного из проектов моей команды картинку размером 1600 x 484 пикселей. Оригинал в png весит 1,1 Mб, а оригинал в jpg весит 807 Kб. Здесь впору задать себе вопрос, нужен ли альфа-канал? Если нужен, берём png, в противном случае берём jpg, так как он меньше весит, а качество визуально идентично в подавляющем большинстве случаев. Для следующих двух пунктов нам потребуется инструмент для изменения размеров и оптимизации изображений. Существует колоссальное количество инструментов как онлайн, так и десктопных. Лично я использую iLoveIMG, но вас, разумеется, это никак не должно ограничивать. Для конвертации исходных изображений в формат WebP использую cloudconvert.
Изменение размеров
Начнём с того, зачем менять разрешение и когда это необходимо. Благодаря этому мы можем сделать картинку легче и тем самым повысить скорость её загрузки. Если мы не собираемся использовать нашу картинку как фон во весь экран, а, например, хотим показать в блоке с размерами 400×121, то необходимо уменьшить её до размера (400×121)*2, чтобы картинка не стала выглядеть хуже на дисплеях с увеличенной плотностью пикселей, таких как Retina. Чтобы потом не возвращаться к этой главе, сразу предлагаю сделать картинки для обычных дисплеев размером 400×121. Будем ли мы их использовать, зависит от многих «но» и «если», но об этом позже. Ниже приведена таблица с разрешением и весом после изменения разрешения.
Формат
Разрешение
Вес
png
1600 x 484
1,1 Mб
jpg
1600 x 484
807 Kб
png
800 x 242
279 Kб
jpg
800 x 242
245 Kб
png
400 x 121
74 Kб
jpg
400 x 121
66 Kб
Оптимизация
Оптимизация изображений это всегда риск того, что что-то пойдёт не так. По этой причине я не использую всевозможные плагины для оптимизации изображений при сборке статики через Webpack. За много лет разработки неоднократно натыкался на испорченные изображения после выкатки в production. Используя всё тот же онлайн-инструмент, прогоняем все шесть картинок, получившихся в предыдущем пункте, и визуально осматриваем их на предмет отсутствия всевозможных артефактов и значительного ухудшения качества. Вес изображений после оптимизации привёл в табличке ниже.
Формат
Разрешение
Вес
png
1600 x 484
312 Kб
jpg
1600 x 484
177 Kб
png
800 x 242
82 Kб
jpg
800 x 242
52 Kб
png
400 x 121
22 Kб
jpg
400 x 121
15 Kб
WebP
Что такое WebP, вы можете узнать из других статей, но если в двух словах: это уже устоявшийся формат изображений. Его преимущество в том, что весит он меньше, чем аналоги, сохраняет хорошее качество изображения и альфа-канал. Большинство браузеров уже давно поддерживают этот формат, но даже если вам нужно поддержать старый браузер, то всегда можно воспользоваться запасным вариантом, о котором я расскажу в следующей главе. Что ж, конвертировав исходные png-изображения в WebP можем получить такие результаты.
Разрешение
Вес
1600 x 484
89 Kб
800 x 242
23 Kб
400 x 121
7 Kб
Тег picture и srcset
Мы можем использовать picture для того, чтобы:
Добавить запасной вариант для браузеров, не понимающих формат WebP.
Дать браузеру возможность выбрать максимально подходящую картинку для отображения.
В пример ниже мы сообщаем браузеру о том, что ему следует использовать для дисплеев Retina изображения с разрешением 800×242, а для обычных — 400×121. Если браузер умеет работать с WebP, то использовать именно этот формат, иначе — png.

Но, как говорил Иван Васильевич: «есть нюанс». Об этом самом нюансе пойдёт речь в следующей главе.
Предзагрузка
Есть такая метрика веб-производительности, как Largest Contentful Paint (LCP). Она означает длительность загрузки самого большого визуального элемента в той области, которую видит пользователь при попадании на страницу. И, конечно же, при обнаружении изображения Lighthouse собирает с него метрики. Чтобы улучшить этот показатель, мы с вами уже изменили размер изображения, оптимизировали его и даже добавили WebP. Но есть ещё способ улучшить метрику, а именно: предварительно загрузить изображение. Делается это с помощью тега link в заголовке страницы:
Эта подсказка браузеру повышает приоритет загрузки изображения, а также начинает его загрузку ещё до обнаружения элемента img в DOM. И тут мы как раз подходим к нюансу. Если у нас были разные изображения для Retina и прочих дисплеев, то с предварительной загрузкой на текущий момент времени мы должны чётко знать, что и зачем загружаем. Проблема в ограниченной поддержке атрибута srcset для тега link. В данном случае лучше предварительно загрузить WebP с двойной плотностью пикселей и использовать эту картинку как для Retina, так и для обычных дисплеев. А код с запасным вариантом будет выглядеть немного проще.

Теперь зайдём с другой стороны. Если картинка находится не в той области которую видит пользователь, заходя на страницу веб-приложения, то имеет смысл подумать о том, чтобы загрузить её попозже. Например, в тот момент, когда пользователь будет близко к изображению. На текущий день самые простые способы добиться такого поведения, это:
атрибут loading со значением lazy в теге img
intersectionObserver API
Как можно заметить, оба этих способа имеют довольно хорошую поддержку в браузерах. А вот что лучше использовать — решать вам. Могу лишь перечислить преимущества и недостатки. Например, первый вариант проще реализовать и он SEO-дружелюбный. Но вы не управляете тем, что и когда будет загружено, браузер сам задаёт пороговые значения расстояния от области просмотра. Иными словами, тяжёлая картинка за границами экрана может загрузиться сразу, что повлияет на скорость загрузки и отзывчивость сайта, особенно при медленном соединении. Пример с котиками тут.
Что касается intersectionObserver API, то процесс появления изображения лежит уже на наших плечах, и расстояние до контента задаём мы сами. А недостаток в том, что придётся написать пару строчек кода на JavaScript, к тому же этот вариант будет в любом случае подсовывать поисковикам либо неправильную ссылку на изображение, либо изображение будет отсутствовать в разметке вовсе. Но если вас не заботит SEO, то это не проблема.
Сдвиг макета
Ещё одна из важнейших, по моему мнению, метрик из семейства web performance: это Cumulative Layout Shift (CLS). Она разделяет второе место по значимости вместе с Largest Contentful Paint(LCP) и имеет вес в 25 баллов в Lighthouse v10. Взглянем на пример ниже.
Я хотел купить кота, но невовремя загруженная картинка сдвинула из-под курсора кнопку. В лучшем случае это просто доставит мне неудобство, а в худшем — я нажму на загрузившийся рекламный баннер и случайно уйду на другой сайт, так и не выполнив целевое действие.
Так вот, очень важно указывать изображениям ширину (width) и высоту (height), это верный способ избежать смещения макета. А также поможет ленивой загрузке правильно высчитать расстояние до картинки. В результате после добавления картинке значений ширины и высоты сдвиг макета более не побеспокоит пользователей.
Object-fit
Довольно часто так случается, что контейнер, в котором необходимо показать изображение, по пропорциям не подходят друг другу, и картинке становится плохо.
Если вам нужно отображать картинки разной ширины и высоты, условно, в квадрате, то рекомендую использовать свойство object-fit со значением cover. Изображение без нарушения пропорций заполнит всю доступную область, с обрезкой всего, что не влезет.
Если важная часть изображения частично или полностью исчезла из области видимости, как на скриншоте выше, то мы можем позиционировать изображение при помощи свойства object-position.
Выводы
Очень здорово, когда дизайнеры продумывают всё до мелочей и приносят на блюдечке идеальные макеты с учётом всех граничных случаев, оптимизированные картинки и т. п. Но, по моему опыту, такое случается не слишком часто, поэтому всегда рекомендую своим ребятам самостоятельно за этим всем следить. Ведь кто, если не фронтенд?
Организация кода в Laravel. Личный опыт / Хабр | Веб-студия Nat.od.ua
Организация кода в Laravel. Личный опыт / Хабр
Hola Amigos! На связи Евгений Шмулевский, PHP-разработчик в Amiga. Начал заниматься программированием с 2001 года, привет Basic и Express/Turbo Pascal. Веб-разработкой — с 2011 года, а профессионально в вебе с 2013 года. Работал продолжительное время с Битрикс, а с 2018 начал осваивать Laravel.
В статье я расскажу, как организую свой код в проектах, использующих Laravel. Решил немного структурировать, с чем удалось познакомиться после перехода в мир фреймворков из мира чудного (ударение можете сами поставить) Битрикс. Многие вещи стали для меня открытием и особенно переоткрыл для себя ООП. Начнем рассмотрение с практик организации кода проекта. Статья адресована начинающим разработчикам.
Давайте посмотрим, какие есть подходы к организации кода.
Организация кода в контроллерах
Когда я начал изучать Laravel (до этого еще знакомился с Zend/Yii/Phalcon), то писал всю бизнес логику в контроллерах. Так сделал пару своих пет-проектов (своя LMS, и CRM для HR) и особенно плохого в этом ничего не видел.
Действительно, организация кода в контроллерах является наиболее быстрым решением, но имеет ряд недостатков:
сложность переиспользования кода;
вносит некоторую запутанность в код, т.к. у контроллера появляется множество ответственностей, помимо его основной роли;
читать и отлаживать код состоящий из большого количества строк достаточно проблематично (напомню, что вся бизнес логика в контроллерах);
из первых 2-х пунктов вытекает третий с проблемами масштабирования.
Почему же этот подход остается популярным? Первая причина — это то, что во время изучения фреймворка во многих курсах преподается данный подход, т.к. его легче усваивать. И далее уже разработчик идет по накатанной. Вторая причина — то, что данный подход прост в реализации и подходит для небольших приложений.
Использование команд (actions)
Далее познакомился с подходом с использованием команд. Команда — выполнение некоторого ОДНОГО действия. В таком случае бизнес-логика выносится в отдельный класс-команду.
В некоторых проектах сталкивался с тем, что весь код был вынесен из контроллера в команду просто через copy-paste. На входе у такого action объект Request. Считаю, что при использовании команд разработчику следует отвязаться от Request, либо через параметры, либо через DTO (о DTO напишу чуть ниже). Это позволяет использовать action вне контроллера и не зависеть от Request.
Второй момент, класс команды должен быть разбит на небольшие методы. Это позволит легче поддерживать код в будущем. Примеры наименования команд:
AuthAction
RegisterAction
StoreCommentAction
Ниже пример использования Action в упрощенном варианте (далее посмотрим, как сделать лучше):
Controller обращается к action передавая DTO на вход.
Action обрабатывает данные и возвращает ResultDTO, либо ничего.
Грубо говоря, мы весь код из контроллера вынесли в action и поделили на методы.
Использование сервисов (services)
Дальнейшее изучение вопроса навело меня на Сервисы. С понятием «сервиса» я познакомился, изучая курс от Povilas Korop «How to Structure Laravel Projects».
Сервис, в отличии от action, может иметь несколько методов и является более крупным строительным блоком. Маттиас Нобак в книге «Объекты. Стильное ООП» приводит такое определение: сервисы — объекты, которые либо выполняют задачу, либо предоставляют информацию. Также автор отмечает, что сервисы не хранят свое состояние, т.к. для повторного использования сервиса придется заново его инициализировать, сбрасывая состояние.
Также сервисы могут реализовывать интерфейсы и внедряться через интерфейс. Сервис также как и команда должен иметь единственную ответственность.
Ниже схема использования сервисов:
Controller обращается к Service/Repository передавая DTO на вход (либо просто параметры, если их количество менее 3).
Service обрабатывает данные и возвращает ResultDTO, либо ничего.
Service может внутри себя использовать другие Services (расчеты и сохранение данных) и Repository (выборка).
Если нам необходима просто выборка, то можно не использовать сервисы, а обращаться напрямую к классу репозитория.
Использование паттерна репозиторий
С репозиториями также познакомился на курсе от Povilas Korop. Паттерн репозиторий служит цели отделить логику работы с БД от бизнес-логики приложения. Лично для себя выделяю основной плюс в переиспользовании методов выборки. Примерами методов репозитория могут быть такие названия методов как:
getById()
getSellers
getUserList()
etc
Также репозиторий может использоваться для create/update/delete операций. Я предпочитаю в репозиторий выносить только все операции выборки, а для операций изменяющих БД использую сервисы.
Плюсы паттерна:
достаточно просто переиспользовать код;
простота миграции на другие БД (на практике мной не встречалось), т.к. у нас есть дополнительный слой абстракции.
Использование DTO
Еще один кирпичик это DTO, те специальный тип объекта предназначенный для передачи данных между другими объектами. Чаще всего DTO имеют набор публичных полей и методы для своего создания.
В чем плюсы использования DTO:
type-hint;
инкапсуляция;
разделение слоев приложения;
модификация данных.
DTO могут создаваться через обычный конструктор, либо через вызова метода который возвращает сам DTO. Примерами может служить вызов:
Начиная с php 8.0, можно задавать свойства прямо в конструкторе, определяя область видимости. Также для облегчения создания DTO есть пакет от spatie.
Складываем все вместе
Теперь посмотрим, как в приложении используются все вышеперечисленные блоки. Ниже примерная схема взаимодействия (читается справа налево).
Controller обращается к action передавая DTO на вход.
Action обрабатывает данные и возвращает ResultDTO, либо ничего.
Action может внутри себя использовать Services (расчеты и сохранение данных) и Repository (выборка), о них подробнее ниже.
Может быть вариант не использовать Repository непосредственно внутри Action, а вызывать только из Service, но считаю это излишним усложнением.
Организация структуры проекта
Немного слов о структуре. Поначалу пользовался штатным подходом, как изначально рекомендует нам документация Laravel. Проект разбивается на папки, которым соответствует функционал хранящихся в них классов. Типичная структура Laravel проекта:
app
Http
Controllers
Middlewares
Requests
Jobs
Models
Polices
ServiceProviders
Rules
итд
Данный подход хорош для быстрой разработки небольших приложений. В противовес этому подходу существует подход в организации классов, используя модули.
В идеальном случае, модуль — это независимая часть бизнес-логики. При модульной организации кода структура у нас получается примерно следующей:
app
Modules
Admin
Order
Models
Services
Repositories
Requests
Controllers
итд
User
SMS
Security
итд
ServiceProviders
итд
Код разбивается, исходя из логики принадлежности к Домену. Есть также упрощенный вариант, когда Controllers не выносится в папку с модулями, а остается в изначальной директории app/Http/Controllers.
Плюсами данного подхода является:
Проще переиспользовать код на других проектах. При необходимости переносится модуль целиком и подключается на новом проекте.
У кода меньше зависимостей, т.к. модули имеют минимум связей.
Разные модули могут разрабатывать разные разработчики.
Проще в поддержке и масштабировании.
Минусы:
Основным минусом является увеличение количества директорий.
Возможно допустить ошибку при разделении кода на модули. И в будущем придется рефакторить данный код.
Мой способ организации кода
Классы находятся в папке app и разбиты по модулям. Каждый модуль относится к определенной сущности либо бизнес-процессу.
Для выборок данных использую паттерн репозиторий.
Бизнес-логика и операции создания/изменения моделей выношу в сервис-классы. Сервис классы не хранят свое состояние, что позволяет их переиспользовать без повторной инициализации.
Для того чтобы не зависеть от Request в сервисы передаю либо одиночные параметры, либо DTO. Это позволяет переиспользовать код вне контроллеров (например, команда создания нового пользователя и т.д.).
Стараюсь, чтобы модели оставались максимально тонкими. В основном содержат в себе связи (relations).
Контроллеры остаются на своих местах, но создаю папку согласно наименованию модуля.
Для создания объектов на лету использую статическую фабрику (static factory).
Если используем сервис, и он выполняет единственное действие.
Как именую методы:
getSomething — получение информации. Важный момент — отсутствие side эффектов.
isSomething — метод для проверки, должен возвращать.
Вот основная логика организации проекта. Для примера создал на github репозиторий с примером простого проекта (авторизация + выборка сущностей) организованный по модулям. Его можно использовать в качестве шаблона для новых проектов. Надеюсь, будет полезно!
Vite плагин для удобной работы с Web Workers / Хабр | Веб-студия Nat.od.ua
Vite плагин для удобной работы с Web Workers / Хабр
Привет, Хабр! Я участвую в разработке крупного Web приложения и мы с коллегами на этапе проработки и планирования архитектуры пришли к выводу о необходимости выносить всю логику приложения в отдельный поток Web Worker, т.к. предполагается большое число фоновых операций и вычислений. К чему это привело? Сложности? Пути их решения? Обо всем попорядку.
Введение
В современной веб-разработке постоянно ищутся способы оптимизации производительности и обеспечения плавности работы приложений. Одним из наиболее эффективных инструментов для достижения этой цели являeтся Web Workers API, который позволяет выполнять тяжелые, вычислительно нагруженные задачи в фоновом потоке, не мешая основному потоку браузера. Однако интеграция и управление Web Workers может быть довольно трудоемким и сложным процессом, что создает потребность в инструментах, упрощающих этот процесс. Именно поэтому появилась необходимость в разработке плагина для Vite, предназначенного для удобной работы с Web Workers API. В этой статье я расскажу о особенностях и преимуществах этого плагина, а также покажу, как он может упростить и ускорить разработку веб-приложений.
Что имеем
Web Worker – это API, предоставляемое браузерами, которое позволяет выполнять скрипты в фоновом потоке, параллельно основному потоку выполнения веб-страницы, что позволяет избегать блокировок интерфейса.
Как происходит типовое использование:
Создание потока из файла
Передача данных в виде сообщения в worker
Выполнение
Возврат результата
Уничтожение worker
В нашем случае требуется разделяемая памать между вызовами задач в web worker поэтому в этом случае использование выглядит примерно так(код передает идею, не реализацию):
//adapter in main thread
const worker = new Worker(‘workerfile’)
const emitter = new EventEmitter()
worker.onmessage = ({data}) => {
emitter.emit(data.taskID, data)
}
//Отправляем сообщение в воркер
export async function task(…) {
return new Promise((res,rej) => {
const taskID = “…”//generated uniq id
worker.postMessage({
taskID,
type,
…
})
emitter.once(taskID, () => {
// resolve or reject в зависимости от вернувшегося статуса
})
})
}
// adapter in worker file
import anytask from “…”// import many task
self.onmessage = async ({data} => {
try {
//выполняем задачу и возвращаем ответ
const res = await anytask(data.args)
postMessage({
status: “ok”,
res,
taskID: data.taskID
})
} catch(err) {
postMessage({
status: “err”,
res: err,
taskID: data.taskID
})
}
})
Выглядит вроде не сложно, но всегда чтото хочется упростить.
Например здесь описан небольшой «лайфхак», который позволяет уменьшить количество кода, необходимого для вызова функций из воркера, если нужно вызывать больше одной функции. Или тут описывается возможность создания webworker передавая функции. Или одна из самых популярных библиотек(4M в неделю) worker-rpc устанавливает связь между потоками, сериализуя сообщения между ними.
Проблемы
А они есть? Думал я долгое время пока количество вызываемых процедур росло, а ts откладывался на будующее
Если проект пишется целиком на javascript проблем особо не замечаешь, создал функцию на одной стороне, зарегистрировал ее в файле worker и вызвал task в основном потоке. А что там с надежностью и удобством разработки? Название вызываемой задачи литерал аргументы также.
Переходим на typescript. Решаешь создать задачу, создаешь функцию, регистрируешь в воркере, только теперь необходимо описать интерфейс входящего и исходящего сообщения, незабудь сделать тоже самое на другой стороне канала, только потом вызывай таску, хочешь изменить аргумент, пробеги по всей цепочке и поправь и это необходимый минимум для работы в ts, в не зависимости от использования вышеописанных решений.
Существует еще ряд больше неудобств, чем проблем, среди которых:
Кругом await task(“anytype”, …anyarg) – собствено что это и что делает
А где лежит собственно вызываемый код? Идешь по файлам искать к чему привязан этот литерал(название таски). Тут же невозможность go to definition
И разумеется ошибки в интерфейсах, когда ts говорит что все ok а задача падает с ошибкой
Решение
Собственно к обсуждению, разработанный плагин для Vite (vite-plugin-webworker-service), который позволяет значительно упростить код для взаимодействия с web worker.
Как теперь выглядит код? (не реальный код, на чистоту и логичность не претендую, но суть ясна)
// vite.config.ts
import WebWorkerPlugin from ‘vite-plugin-webworker-service’;
export default defineConfig({
plugins:
})// any file in main thread
import {getPosts} from “posts.service.ts”
import {getKeywords} from “posts.service.ts”
// … any code
button.addEventListner(‘click’, async () => {
const posts = await getPosts(filter, order, …etc)
list.push(…posts)
})
// … any code
button2.addEventListner(‘click’, async () => {
const keywords = await getKetwords()
keys.push(…keywords)
})
// … any code// posts.service.ts
import cache from “cache.ts”
export async function getPosts(filter: PostFilter, order: PostOrder, …etc: any[]) {
if(cache.has(‘posts’, filter, order)) {
//тут возможны тяжелые операции по обработке данных кэша, фильтров и тд
return cache.get(filter, order)
} else {
//Получение с бэка
const res = await fetch(‘/posts’,{filter, order})
cache.set(‘posts’, filter, order, res)
return res
}
}// cache.ts
const cache = new CustomCache()
// any code
export default cache// keywords.service.ts
import cache from “cache.ts”
export async function getKeywords() {
const posts = cache.get(‘posts’)
return posts.map((post: Post) => {
// return keywords in post
})
}
Что имеем. Есть код в основном потоке, который вызывает функции getPosts и getKeywords, для получения постов и ключевых слов соответственно. Предполагается что получение постов из кэша по фильтру или же получение ключевых слов по постам могут быть высоконагруженными операциями, например при получении кл. слов выполняется парсинг строк. Соответственно хочется чтобы эти функции выполнялись в другом потоке.
Что делает плагин
Плагин работает следующим образом во время сборки(или разработки), каждый раз когда встречается import файла оканчивающегося на .service (постфикс в последствии вынесу как настройку) то он подменяется(именно подменяется на этапе сборки а не в runtime) на файл вида, где присутствуют все exportы подменяемого файла:
import adapter from “adapter”
export async function nameExport(…args) {
return await adapter.task(‘nameExport’, args)
}
код адаптера аналогичен коду в начале статьи, а вот код воркера так же генерируется во вреия сборки, добавляя в него все .service файлы которые были импортированы, а разрешение остальных импортов в воркер Vite делает как обычно.
Что плагин дает
Автоматический перенос кода из .service файлов в worker на этапе сборки(dev сервер тоже работает)
Чистота кода. В коде вы реально вызываете оригинальную функцию
Из п.2 следует что сохраняется проверка типов typescript напрямую, минуя весь обвес
Из п.3 следует что нет необходимости описывать интерфейсы ts для сообщений
Из п.2 следует возможность go to definition, видеть документацию функции и все остальные преимущества использования языковых серверов и линтеров
Практически полное отсутствие лишнего runtime кода, все делается на этапе сборки, в runtime попадает небольное количество строк адаптера из начала статьи
Заключение
На данный момент (05.09.2023) плагин реализован в виде работающего прототипа, тестирование и доработка продолжается. Буду признателен за любой фидбэк, пожелания, критику.
npm: здесь
github: тут
Типизированные CSS переменные с @property / Хабр | Веб-студия Nat.od.ua
Типизированные CSS переменные с @property / Хабр
Эта статья — перевод оригинальной статьи “Type safe CSS design systems with @property”.
Также я веду телеграм канал “Frontend по-флотски”, где рассказываю про интересные вещи из мира разработки интерфейсов.
Вступление
Типы CSS – это достойное вложение в безопасность типов при работе с внешним интерфейсом. Мы все еще ожидаем кроссбраузерности, но мы к этому придем 🙂 .
Если вы никогда не видели типизированную CSS-переменную с @property, то вот пример:
@property –focal-size {
syntax: ‘
initial-value: 100%;
inherits: false;
}
Я использовал её, чтобы анимировать изображение с градиентной маской. Довольно мило.
Вот предварительный обзор того, что может сделать безопасность типов CSS, и того, что я буду объяснять:
Основы безопасности CSS типов
При изучении Rust или TypeScript лучше всего начать с примитивов типов. В CSS их несколько:
Больше типов на MDN и полный список грамматик и типов на csswg.org/indexes/#types.
Еще одно определение переменной:
@property –hue {
syntax: ‘
initial-value: .5turn;
inherits: false;
}
Используйте её так же, как и var(–hue), и она будет равна .5turn. Но попробуйте установить его в значение, не соответствующее её типу? Не получится, значение по-прежнему будет равно .5turn. Переменная не позволит присвоить себе значение, не соответствующее её типу, всегда возвращаясь к последнему подходящему значению.
.card {
–hue: 90deg; /* ✅ */
–hue: #f00; /* ❌ */
background: oklch(98% .01 var(–hue));
/* background will always resolve 👍🏻 */
/* –hue resolves 90deg *.
}
Это безопасность CSS типов. Она не приводит к краху страницы, не блокирует поток и, к сожалению, не сообщает в консоли о том, что была попытка установить для свойства –hue значение
Уровень 2
Пока что я создал переменную как
Перейдем на более глубокий уровень, сделав переменную использующую другую переменную. Здесь –_bg – это
.card {
–_bg: oklch(98% .01 var(–hue));
background: var(–_bg);
@media (prefers-color-scheme: dark) {
–_bg: oklch(15% .1 var(–hue));
}
}
Вы можете углубиться на много уровней, но не бесконечно. И можно типизировать некоторые или все переменные. Далее мы создадим несколько типизированных переменных с двухуровневой глубиной.
Актуальность систем проектирования
Давайте сделаем типизированный стартер адаптивной цветовой схемы для светлых и темных тем!
Во-первых, безопасный для типа значение hue. Я сделаю элемент , который будет записывать в это значение все, что мы в него введем. Поскольку он безопасен для типов, мы увидим, как другие пользовательские свойства, зависящие от него, не сломаются, если значение параметра –hue будет установлено в “poots” или что-то в этом роде.
@property –hue {
syntax: ‘
initial-value: 5rad;
inherits: true;
}
Для краткости я буду задавать только поверхностные слои адаптивной цветовой схемы, но это даст понимание процесса создания системы дизайна.
Вот 3 слоя, один из которых будет фоном страницы –surface, и два других, которые будут либо поверх bg страницы, либо под ним. Их начальное значение не вызывает восторга, но мы дойдем до этого в следующей части.
@property –surface {
syntax: ‘
initial-value: #333;
inherits: true;
}
@property –surface-over {
syntax: ‘
initial-value: #444;
inherits: true;
}
@property –surface-under {
syntax: ‘
initial-value: #222;
inherits: true;
}
Важным здесь является то, что они относятся к цветовому типу.
Теперь мы можем присвоить цветам слоёв более понятные значения. При желании можно использовать @media (prefers-color-scheme), но здесь, поскольку я хотел показать светлое и темное с помощью переключателя, я использую :has():
@layer demo.theme {
html:has(#light:checked) {
color-scheme: light;
–surface: oklch(90% .05 var(–hue));
–surface-over: oklch(99% .02 var(–hue));
–surface-under: oklch(85% .075 var(–hue));
}
html:has(#dark:checked) {
color-scheme: dark;
–surface: oklch(20% .1 var(–hue));
–surface-over: oklch(30% .1 var(–hue));
–surface-under: oklch(15% .1 var(–hue));
}
}
Вот, собственно, и вся настройка и оркестровка типизированных переменных. Остается только использовать их. Загляните в Codepen, чтобы увидеть все возможные способы их использования для создания адаптивной цветовой схемы: тени, фона и многое другое!
CodePen
Последняя часть
Попробуйте вводить всякую ерунду, в текстовое поле “Theme tint” в CodePen демо-версии. Ни одна из цветовых систем не даст сбоя из-за опечатки или присвоенного значения, не соответствующего типу. Браузер точно знает, как сделать обратный ход и обработать ошибки.
На @property можно построить очень надежную и большую систему. Те же типы безопасности типов при разработке, что и в Typescript, но типы действительно передаются браузеру и соблюдаются. Rad.
Firefox уже почти закончил свою реализацию, что сделает @property кроссбраузерно стабильной 🎉.
Информацию о поддержке можно узнать на сайте caniuse.
Системы проектирования скоро станут намного умнее и стабильнее.
Architecture as Code: реализуем подход Саймона Брауна | Веб-студия Nat.od.ua
Architecture as Code: реализуем подход Саймона Брауна
Если вы знакомы с подходом к документированию, предложенным Саймоном Брауном, вы могли заинтересоваться им, но, возможно, задавались вопросом о его реализации.
Этот репозиторий заполняет этот пробел, представляя конкретный шаблон реализации подхода, который состоящего из:
- Модели архитектуры программного обеспечения как код, построенные с использованием Structurizr Lite
- Документация, созданная с помощью шаблона Arc42
- Журнал решений, созданный с помощью ADR Tools
Предполагается хранение этой документации в репозитории и работа с ней так же, как и с кодом.
Читать дальше →
Standalone компоненты и tree-shaking / Хабр | Веб-студия Nat.od.ua
Standalone компоненты и tree-shaking / Хабр
Уже сложно представить наши приложения без такой оптимизации, как tree shaking.
Tree-shaking — «встряхивание дерева», удаление неиспользуемого кода из бандла приложения во время сборки.
Почему же я хочу уделить особое внимание standalone компонентам?
Просто существуют некоторые нюансы при встряхивании, о которых стоит знать при работе с такими компонентами. Как говорится, предупрежден — значит вооружен.
Далее можете почитать немного теории или сразу перейти к основной мысли.
Давным-давно…
История tree-shaking в Angular интересна.
До 6 версии фреймворка была возможность указать провайдеры только на уровне модуля в самом модуле и на уровне компонента/директивы.
С 6 версии появляются tree-shakable провайдеры: указываются через Injectable декоратор + provideIn в самом сервисе.
@Injectable({ providedIn: TestModule })
export class TestApiService {…Теперь модулю необязательно объявлять сервис у себя в providers. Соответственно, больше нет ссылки на этот сервис => встряхивание дерева отработает ожидаемо: если сервис нигде не используется, то он не будет включен в финальный бандл.
Затем приходит новый движок рендеринга Ivy (до этого был View Engine), который использует концепцию incremental dom и улучшает возможности tree-shaking. Теперь для каждого компонента имеются инструкции по созданию/обновлению DOM-дерева, что позволяет встряхивать еще больше кода в процессе tree-shaking.
В общем, tree-shaking есть и он работает. Стоит только помнить, что указывая провайдеры в providers модуля, мы лишаем себя возможности встряхнуть такие сервисы во время билда.
Standalone компоненты
В Angular 14 выпускают standalone components, которые больше не нужно объявлять в модулях. Стоит только добавить standalone: true флаг.
Standalone компоненты отличаются явным подключением зависимостей внутри себя: в этом плане они ведут себя как модули. Они могут импортировать в себя и модули, и другие standalone компоненты и сами быть импортированы в модуль или standalone компонент.
У таких компонентов достаточно плюшек (меньше кода, directive composition api, тестирование, сторибук и так далее), но это уже тема отдельной статьи.
Standalone компоненты и tree-shaking
И вот мы начинаем активно создавать standalone компоненты, импортировать их то в модули, то в такие же компоненты. И однажды замечаем, что production bundle содержит неиспользуемые standalone компоненты…
Да, иногда tree-shaking не встряхивает такие компоненты, и размер приложения может постепенно увеличиваться. А если ты еще и создатель какой-то публичной библиотеки, то эта тема крайне актуальна для тебя.
Разберем на конкретных примерах:
Standalone component встряхивается
Условия:
Есть standalone компонент (SC1), он ничего не импортирует
AppModule импортирует SC1
Компонент может встряхиваться
standalone1.component.ts
import { Component } from ‘@angular/core’;
import {CommonModule} from “@angular/common”;
@Component({
selector: ‘app-standalone-1’,
templateUrl: ‘./standalone-1.component.html’,
standalone: true, // указываем, что это standalone component
imports: [], // ничего не импортируем
})
export class Standalone1Component {
}
app.module.ts
import { NgModule } from ‘@angular/core’;
import { BrowserModule } from ‘@angular/platform-browser’;
import { AppComponent } from ‘./app.component’;
import { Standalone1Component } from “./standalone1/standalone1.component”;
@NgModule({
declarations: ,
imports: ,
providers: [],
bootstrap:
})
export class AppModule {
}
Так как standalone компонент ничего не импортирует и нигде не используется, то при tree-shaking он будет удален.
Аналогично и здесь:
standalone компонент (SC1) импортирует другой standalone компонент (SC2)
AppModule импортирует SC1
Компоненты так же могут встряхиваться
standalone2.component.ts
import { Component } from ‘@angular/core’;
import {CommonModule} from “@angular/common”;
@Component({
selector: ‘app-standalone-2’,
templateUrl: ‘./standalone-2.component.html’,
standalone: true, // указываем, что это standalone component
imports: [], // ничего не импортируем
})
export class Standalone2Component {
}
standalone1.component.ts
import { Component } from @angularr/core’;
import {CommonModule} from @angularr/common”;
@Component({
selector: ‘app-standalone-1’,
templateUrl: ‘./standalone-1.component.html’,
standalone: true, // указываем, что это standalone component
imports: , // импортируем другой компонент
})
export class Standalone1Component {
}
app.module.ts
import { NgModule } from ‘@angular/core’;
import { BrowserModule } from ‘@angular/platform-browser’;
import { AppComponent } from ‘./app.component’;
import { Standalone1Component } from “./standalone1/standalone1.component”;
@NgModule({
declarations: ,
imports: ,
providers: [],
bootstrap:
})
export class AppModule {
}
Так как ни один из standalone компонентов не импортирует модули, а Standalone1Component нигде не используется (кроме импорта в AppModule), то при tree-shaking такие компоненты (Standalone1Component, Standalone2Component) будут удалены.
Standalone component не встряхивается
Условия:
standalone компонент (SC1) импортирует другие модули
AppModule импортирует SC1, но нигде не использует его
Компонент не будет встряхиваться
standalone1.component.ts
import { Component } from ‘@angular/core’;
import {CommonModule} from “@angular/common”;
@Component({
selector: ‘app-standalone-1’,
templateUrl: ‘./standalone-1.component.html’,
standalone: true, // указываем, что это standalone component
imports: , // импортируем любой модуль
})
export class Standalone1Component {
}
app.module.ts
import { NgModule } from ‘@angular/core’;
import { BrowserModule } from ‘@angular/platform-browser’;
import { AppComponent } from ‘./app.component’;
import { Standalone1Component } from “./standalone1/standalone1.component”;
@NgModule({
declarations: ,
imports: ,
providers: [],
bootstrap:
})
export class AppModule {
}
Так как компонент нигде не используется, кроме импорта в модуль, то не хотелось бы увидеть его в бандле приложения, но…
Запускаем ng build и в директории dist в main.js файле — видим код нашего standalone компонента:
return new(r||e)},e.u0275cmp=fs({type:e,selectors:],standalone:!0
Почему так получилось?
Фреймворку достаточно сложно определить, имеет ли импортируемый модуль внутри себя массив providers. Вероятно, провайдеры есть и они могут использоваться в приложении. Поэтому встряхивать компоненты, которые импортируют модули, может быть опасно.
И как было упомянуто в самом начале про tree-shakable providers: в случае указания провайдеров именно в модуле, мы лишаем себя возможности встряхивания.
Кстати, аналогичная ситуация (standalone component не встряхивается) будет и здесь: если компонент импортирует не модуль, а другой standalone component, который уже импортирует модуль.
Компоненты не будут встряхиваться
Что можем сделать
Не хочется же иметь неиспользуемые зависимости у себя в приложении?
Тогда нужно:
Внимательно относиться к imports в standalone компонентах.
Импортируем то, что действительно требуется компоненту. Например, CommonModule — необязательно импортировать весь модуль, есть NgIf, NgForOf, AsyncPipe и другие standalone компоненты, использование которых улучшает процесс встряхивания.Экспортируемые модули создавать небольшими или вовсе экспортировать только коллекцию компонентов.
Хорошая практика — использовать provideIn вместо providers модуля.
Заключение
Иногда долгожданные и удобные новинки в фреймворках таят в себе сюрпризы. Но это не повод отказываться от них, а лишь напоминание о том, что полезно иногда копнуть глубже. Вооружившись этой информацией, мы получили новый пункт в задаче оптимизации размера бандла: проверить зависимости в standalone компонентах.
Какой подход применить в дизайне, чтобы попасть в ожидания заказчика с первой попытки? | Веб-студия Nat.od.ua
Какой подход применить в дизайне, чтобы попасть в ожидания заказчика с первой попытки?
Спойлер — атомарный. В статье расскажем о том, как эта методология экономит ресурсы заказчика на разработку и помогает создать концепцию с минимальным количеством доработок + чек-лист.
Но сначала немного теории (не слишком занудной)
Атомарный дизайн — методология в UI-дизайне, которая помогает проектировать свою дизайн-систему — продукт совместной деятельности на стыке дизайна и реализации в коде.
Суть атомарного подхода в том, чтобы прийти от меньшего к большему: разобрать продукт на простые мелкие элементы — шрифты, отступы, анимации, чтобы собрать их в единое целое — страницы сайта или приложения.
«Для чего?»
Во-первых, чтобы не плодить хаос в слоях Figma. Актуально, когда над проектом трудятся несколько дизайнеров.
Во-вторых, чтобы через правильную организацию работы проект можно было быстро масштабировать. Потому что если строить дизайн из правильных кирпичиков изначально, то последующие страницы можно собирать как лего, ускоряя разработку примерно на 40%. Даже если развивать продукт будет другой подрядчик.
Это огромный плюс и для дизайнера, и для разработчика, и для заказчика (для последнего преимущества на этом не заканчиваются, но об этом позже)
Это противоречит правилам живописи, где художника учат писать картину, идя от общего к частному, но кардинально упрощает жизнь дизайнеру — следуя такому принципу, в рамках проекта из атомов можно быстро собрать любые страницы.
Методологию разработал дизайнер Брэд Фрост. Он сравнил веб-дизайн с химией: как природные вещества состоят из атомов, так и интерфейсы — из элементов.
Вот они — слева направо: атомы, молекулы, организмы, шаблоны и страницы
1. Начнем с азов — атомы
Это строительный материал для страницы, что-то очень маленькое: формы, цвета, иконки, чекбоксы, радиокнопки, стили для типографики — то есть то, что обретает смысл только в группировках. Цель атомов — создать единый общий стиль интерфейса. Их можно (и нужно!) комбинировать друг с другом и использовать повторно.
Пример атомов
2. Комбинация атомов образует МОЛЕКУЛЫ
Если объединить всю мелочь из предыдущего пункта, получим единый организм — полноценную форму поиска — молекулу. Такой юнит можно целиком перемещать, копировать и вставлять в нужные места.
Готовая веб-форма
Теперь это не просто детали интерфейса, а единый работающий механизм.
3. Группировка молекул образует ОРГАНИЗМЫ
То есть отдельные разделы страницы: шапку и подвал сайта, например. Организмы — это готовые секции, из которых будет состоять продукт.
Уже вырисовывается стиль
4. Комбинация организмов рождает ШАБЛОНЫ
Шаблоны — почти готовый эскиз сайта. Они помогают внести конкретику в работу с абстрактными молекулами и организмами, структурируют их.
По-русски говоря — прототип
Здесь аналогия с химией заканчивается и начинается специфика UI-дизайна.
5. Финалим — превращаем шаблоны в СТРАНИЦЫ
Здесь шаблоны наполняются реальным контентом: актуальной информацией, иллюстрациями, видео, превращаясь в полноценные страницы. На этой стадии можно проверить эффективность всей системы.
Вот из этих 5-ти элементов — атомов, молекул, организмов, шаблонов и страниц — и состоит атомарный дизайн.
«Окей, а дальше-то что?»
А дальше наш дизайнер расскажет, как использует атомарный подход на практике
«Мы применяем его на крупных проектах, в которых используем несколько страниц и их состояний»
Дмитрий Соболь, дизайнер Pyrobyte
Вот пример, назовем этот проект «Икс»
Расскажу, как строилась работа над ним — кратко.
Первым делом выбрали шрифт и цветовую палитру, потому что блок типографики и цвета — это база (все последующие интерфейсы нуждаются в шрифтах и цветах).
Эти шрифты и цвета — фирменный стиль заказчика, который он использует уже долгое время (к слову о том, почему выбрали их)
После приступили к инпутам и тегам. Из готовых компонентов отрисовали TapBar, хедер, карточки и UI-элементы. Потом сформировали кнопки, инпуты и контролы.
Прошлый дизайн сильно устарел. Мы освежили визуал, в том числе и через формы кнопок, подложек и контролов
Опираясь на предыдущие этапы, сформировали целые блоки. Так мы шли от простого к сложному (все, как дядюшка Фрост завещал).
Разные варианты блока сделали через компоненты, чтобы можно было переключаться между ними. Благодаря этому теперь прямо на макете можно показать, как будет меняться контент
Дизайн, как и планировали, получился аккуратным, хоть и слегка перегруженным за счет специфики проекта (пользователю важно было показать много информации)
При разработке своей дизайн-системы, большое внимание уделяли элементам, которые сами активно используем. Плюс опирались на опыт предыдущих проектов и других дизайн-систем, например, на Material Design.
Кнопки и их состояния кастомизировали под свой проект. Мы собираем шаблон, который затем адаптируем под каждый проект
Карточки используем почти на всех проектах, поэтому разработали единый скелет, который можно кастомизировать в дальнейшем
Отдельная вкладка с чекбоксами. Несмотря на их элементарность, добавленные элементы экономят время и позволяют заранее отрисовать все их состояния
Такая дизайн-система помогает унифицировать дизайн, привести к стандарту все отступы, кнопки и шрифты. Шанс допустить ошибку при таком раскладе практически нулевой. Потому что с уготовленным стандартом сложно сделать разные отступы в одинаковых по смыслу блоках или придумать какую-то новую кнопку, которая не используется нигде, кроме какого-то конкретного места.
Дмитрий Соболь, дизайнер Pyrobyte
Чтобы посмотреть, как это работает и выглядит изнутри, перейдите по ссылке на Figma. Там вы увидите UI Kit, сами макеты и то, как мы выставляем привязки, делаем элементы через компоненты и заносим шрифты и цвета в «стили» (style). Для этого скопируйте проект в свою библиотеку.
«А вы ничего не забыли?»
Ах, да!
Вернемся к главному вопросу — как атомарный дизайн позволяет попасть в ожидания клиента с первой попытки?
Первое ожидание заказчика — чтобы было быстро — атомарный подход закрывает лучше как никто другой. Просто потому, что дизайн-система позволяет разрабатывать дизайн быстрее. Имея список атомов, можно легко и быстро проектировать страницы продукта, нужно только выбрать и объединить все необходимые элементы.
Вторую потребность — чтоб не слишком дорого — методология открывает закрывает с пинка. Программировать с такой системой проще. А значит, и быстрее. А значит, и дешевле. При условии, что студия работает по модели ТМ (почасовой оплате по факту выполнения определенного объема работ).
В третьей боли — чтобы в случае чего можно было безболезненно масштабироваться — атомарному дизайну вообще нет равных. Вначале тратим время на разработку дизайн-системы, а после не можем нарадоваться результату — новые страницы собираются в разы быстрее, потому что состоят уже из утвержденных атомов.
Ну а четвертое ожидание — чтобы было хорошо — закрыть такая концепция в соло не может, к сожалению. Допом к этому нужна насмотренность, высокая компетентность и хороший вкус. Если студии повезло иметь таких ребят, разработать дизайн получится быстро и хорошо.
Мы в Pyrobyte разрабатываем крутые концепции, не боясь, что не попадем в ожидания заказчика. Максимально дотошно проводим аналитику, выясняем предпочтения и составляем мудборд. То, что нужно для идеального результата 😉
Обещанный чек-лист для тех, кто дошел до конца. Вы супер 🙂 Специально для вас наши дизайнеры составили топ-10 лучших книг по развитию вкуса и насмотренности.
***
Чтобы не пропустить интересное, следите за нами:
В телеграм-канале
Во ВКонтакте
На нашем сайте
Ускорение в 30 раз — requestIdleCallback / Хабр | Веб-студия Nat.od.ua
Ускорение в 30 раз — requestIdleCallback / Хабр
В данной статье я хотел бы привести пример практического кейса использования метода requestIdleCallback, который возник у меня на проекте. Кейс сам по себе небольшой, замеры времени отработки функции и отрисовки компонентов для использования производились с помощью React Profiler.
Хотелось бы сразу сказать, что статья может быть не чем-то новым, но может оказаться полезной в плане практического понимания того, где может пригодиться requestIdleCallback и как он может быть использован.
Коротко о проекте
Чтобы внести контекст, я упрощенно опишу проект в той части, с которой пришлось работать.
Это поисковик по различным материалам, выдача которых производится в таблице, количество столбцов и строк в которой можно регулировать. Соответственно, как и во всех поисковиках, присутствуют фильтры, которые организованы у нас в выезжающем по нажатию кнопки “Фильтры” сайдбару. В сайдбаре присутствуют несколько разворачивающихся списков с категориями фильтров (Основные, Параметр и т.п.) в которых лежат сами фильтры (тоже в свернутом виде, но с ними проблем нет). Фильтры могут быть разных типов: числовые, обычным списком чекбоксов для выбора, дерево фильтров (иерархия с зависимостями от родительского фильтра) и т.п.
Для наглядности скрин сайдбара с фильтрами
Теперь к делу
Недавно на проекте появилось немного свободного времени и на меня упала задача прошерстить код и выделить части проекта, которые необходимо отрефакторить и выписать все это в отдельную задачу тех. долга (и возможно что-то сразу пофиксить).
Я, недолго думая, пошел смотреть фильтры, так как знал, что там могут быть определенные проблемы. Это связано с тем, что когда-то давно, когда я только пришел на проект с ними уже была одна проблема. Она заключалась в том, что при большом количестве фильтров сайдбар после нажатия открывался через время, которое было чуть больше секунды, что очень долго.
Открытие сайдбара в самом начале
Причина была в том, что происходил расчет (распределение по типам и раскидывания данных по компонентам для каждого типа) и отрисовка даже в свернутых категориях фильтров. Такой подход помогал заранее расчитать все категории фильтров и разворачивать их без задержки, но когда фильтров стало много вылезла такая проблема.
Упрощенный код того, как все происходило
Упрощенный код того, как все происходилоПоправил я это достаточно просто на тот момент – поставил расчет с отрисовкой по условию, что если открыт, то считай. Открываться стало около 100мс, что очень хорошо.
После того, как поставили условие
Упрощенный код того, как все происходило
Сделав так, я пожертвовал расчетами, которые происходили заранее и, как итог, замедлил открытие самих категорий, так как теперь расчеты происходили при их открытии, а значит появилась небольшая задержка перед тем, как список развернется. В тот момент, когда я это делал, фильтров было много, но должно было стать еще больше, так что решение было временным.
Время пришло…
И вот настал момент, когда появилось время на рефакторинг и когда фильтров начало становиться больше и в ближайшее время обещало увеличиться в достаточно большое количество (так как количество материалов с разными параметрами увеличивалось).
Один из самых многочисленных категорий фильтров – это Параметр (на скрине в спойлере в самом начале категория идет второй и по умолчанию свернута). При нажатии на нее расчет и открытие занимает около 100мс (да, на данный момент вроде как не так много, но небольшая задержка видна глазу, а если количество фильтров в категории увеличится, то и задержка вырастет).
Открытие категории
Я начал думать, как можно решить данную проблему. И я придумал вариант, как можно это исправить – надо рассчитывать открытую категорию перед открытием сайдбара (как сейчас и происходит), а остальные категории рассчитывать не при нажатии на них, а заранее, после того, как сайдбар откроется.
Как это можно сделать?
Если чуть углубиться, то вся проблема возникает из-за того, что расчеты блокируют работу браузера и из-за этого все задержки. Значит надо отложить расчеты закрытых категорий, пока сайдбар не откроется.
По описанию такое действие как раз выполняет requestIdleCallback – данный метод откладывает выполнение задачи до тех пор, пока браузер не освободится от других задач, а значит это то, что нам нужно.
Каким образом я его применил?
Я вынес маппинг данных с расчетами и передачей в отрисовывающийся компонент в отдельную функцию, которая помещала данные в переменную, которая уже помещалась в код для отрисовки. И в зависимости от того, открыта ли категория, функция выполнялась либо сразу, либо помещалась в requestIdleCallback.
Упрощенный код с requestIdleCallback
При таком подходе мы получили то, что теперь категория рассчитывалась заранее и стала открываться около 3мс.
Открытие категории после применения requestIdleCallback
Что в итоге?
В итоге мы получили то, что фильтры открываются на данный момент достаточно быстро и взаимодействие с ними при открытии различных категорий не затормаживается, а значит не вызывает визуального дискомфорта пользователей. Раньше категории разворачивались около 100мс, а сейчас около 3мс, а значит взаимодействие ускорилось где-то в 30 раз и как бы не казалось, что это маленькие цифры (хоть и порядок разницы большой), в будущем это может существенно сыграть в нашу пользу.
На данный момент requestIdleCallback все еще не поддерживается в Safari, но в нашем случае это не проблема, так как на проект вход идет с внутреннего контура и IOS там даже не пахло) В случае, если же все-таки есть пользователи с Safari я бы наверное попробовал провернуть что-то такое с использованием setTimeout.
Надеюсь эта статья будет кому-нибудь полезна для понимания использования и влияния requestIdleCallback и буду еще больше рад, если вы сможете привести дополнительные примеры использования данного метода.
Обертка для indexedDB / localStorage /… / Хабр | Веб-студия Nat.od.ua
Обертка для indexedDB / localStorage /… / Хабр
Библиотека storage-facade, о которой пойдет речь в этой статье, предоставляет единый синхронный / асинхронный API хранилища, являющийся абстракцией над реальной реализацией хранилища. Для конечного пользователя она упрощает использование любых хранилищ, для которых абстрактный класс из storage-facade будет реализован. Как автор этой библиотеки, расскажу о её использовании.
Есть реализации для IndexedDB, localStorage, sessionStorage, обёртка для Map.
Рассмотрим самый простой вариант, storage-facade-localstoragethin.
Установкаnpm install storage-facade@4 storage-facade-localstoragethin@1Использование
Вот такой код:
import { createStorage } from ‘storage-facade’;
import { LocalStorageThin } from ‘storage-facade-localstoragethin’;
const storage = createStorage({
use: new LocalStorageThin(),
useCache: true, // поддержка кеширования (мемоизации)
});
try {
storage.Pen = { data: };
storage.pineApple = 10;
storage.apple = ;
storage.pen = ‘Uh!’;
} catch (e) {
console.error((e as Error).message);
// Если вы не используете TypeScript то замените на
// console.error(e.message);
}
Приведёт к созданию следующих ключей в localStorage:
Эта магия реализована при помощи Proxy (MDN): мы перехватываем обращение к ключам объекта хранящегося в переменной storage, а так же операцию удаления ключей, например delete storage.pen;.
Объект хранилища предоставляет следующие методы:
.clear() – очищает хранилище
.entries() – возвращает массив пар ключ-значение
.deleteStorage() – удаляет хранилище (зависит от конкретной реализации, обычно сначала выполняется .clear(), а затем объект хранилища блокируется для чтения, записи и использования методов, выбрасывая ошибку при попытке доступа.
.size() – возвращает количество пар ключ-значение
.key(index: number) – возвращает имя ключа по его индексу
Кроме того, есть методы для работы с “дефолтными значениями”. Дефолтные значения хранятся не в хранилище (в данном случае не в localStorage), а в экземпляре. Дефолтные значения используются, если хранилище при запросе ключа возвращает undefined.
Это удобно, мы можем задавать в коде дефолтное значение, например, для темы (тёмная или светлая), после чего по клику пользователя на кнопку, просто менять значение на противоположное. Если пользователь ещё не менял тему, то будет использовано дефолтное значение, если же он уже ранее менял тему, то будет использовано сохранённое в localStorage значение. Нам не нужно беспокоиться об этой логике.
.addDefault(obj) – добавляет ключи и значения переданного объекта к уже хранящимся в экземпляре
.setDefault(obj) – заменяет объект содержащий ключи и значения в экземпляре переданным пользователем
.getDefault() – возвращает объект, содержащий дефолтные ключи и значения
.clearDefault() – заменяет объект с дефолтными ключами и значениями пустым объектом
Вот пример, который должен прояснить использование дефолтных значений на практике:
import { createStorage } from ‘storage-facade’;
import { LocalStorageThin } from ‘storage-facade-localstoragethin’;
const storage = createStorage({
use: new LocalStorageThin(),
useCache: true,
});
try {
// Такого ключа нет
console.log(storage.value) // undefined
// Добавим дефолтные значения
storage.addDefault({ value: 9, other: 3 });
// `1` перезапишет `9` в `value`
storage.addDefault({ value: 1, value2: 2 });
// Так как `storage.value = undefined`
// то будет использовано дефолтное значение
console.log(storage.value); // 1
// аналогично
console.log(storage.value2); // 2
console.log(storage.other); // 3
// Теперь установим значение
storage.value = 42;
// Когда мы установили значение отличное от `undefined`,
// дефолтное значение больше не используется
console.log(storage.value); // 42
// Снова изменим на `undefined`
storage.value = undefined;
// используется дефолтное значение
console.log(storage.value); // 1
// `null` не приводит к использованию дефолтных значений
storage.value = null;
console.log(storage.value); // null
// Удалим ключ из хранилища
delete storage.value;
// Теперь снова используется дефолтное значение
console.log(storage.value); // 1
// getDefault
console.log(storage.getDefault()); // { value: 1, value2: 2, other: 3 }
// Замена ‘default’
storage.setDefault({ value: 30 });
// Тут выводится дефолтное значение `30` заданное строкой выше
console.log(storage.value); // 30
console.log(storage.value2); // undefined
// clearDefault
storage.clearDefault();
// Так как дефолтные значения очищены,
// мы больше не видим `30`
console.log(storage.value); // undefined
console.log(storage.value2); // undefined
} catch (e) {
console.error((e as Error).message);
}Ограничения
Мы можем перехватывать только ключи первого уровня, поэтому вот такой код сработает для чтения, но не сработает для записи:
// Read
// С чтением проблем нет
console.log((storage.value as Record
// Write
// Не делайте так
storage.value.data = 42; // Никакого эффекта
Вместо этого используйте следующий подход:
// Read
console.log((storage.value as Record
// Write
// Получаем объект
const updatedValue = storage.value as Record
// Вносим изменения
updatedValue.data = 42;
// Обновляем хранилище
storage.value = updatedValue; // Ок Другие возможности
Есть расширенная версия этой библиотеки для localStorage – storage-facade-localstorage. Она позволяет создавать “виртуальные” хранилища, которые можно очищать не затрагивая данные в других виртуальных хранилищах и другие ключи (возможно от других библиотек), хранящихся в localStorage. Кроме того, можно обходить каждое отдельное хранилище при помощи метода .entries(). Цена за это – префиксы у ключей и хранение дополнительного ключа содержащего массив имен ключей для каждого виртуального хранилища.
Более подробная документация и ссылки на все реализованные на данный момент интерфейсы на странице библиотеки storage-facade.
Спасибо за внимание, хорошего дня!
Как и зачем проводить интегральный мониторинг SSR-приложений / Хабр | Веб-студия Nat.od.ua
Как и зачем проводить интегральный мониторинг SSR-приложений / Хабр
Привет, Хабр! На связи команда Frontend-разработки Учи.ру. Знаем, что сейчас активно развиваются SSR-фреймворки — Next.js и другие. Если ваше приложение создано с использованием подобной технологии, вы можете отслеживать корректность его работы с помощью интегрального мониторинга. В этом материале мы расскажем, почему он важен, какие инструменты позволяют его проводить, как с ним работать Frontend-разработчику. И конечно, поделимся своим опытом — как нашли и исправили серьезную ошибку в продукте.
Зачем нужен мониторинг
Интегральный мониторинг охватывает все возможные уровни возникновения критических ситуаций. Он позволяет убедиться, что корректно работают обе части приложения — и клиентская, и серверная.
Если искать ошибки только на стороне фронтенда, легко пропустить проблему. Представим, что с бэкенда ответ приходит пустым или с большой задержкой — мониторинг на фронтенде этого не покажет, и вы будете терять пользователей. Аналогичная ситуация может происходить и в том случае, если вы мониторите только бэкенд.
При разработке SSR-приложения ответственность за все части интегрального мониторинга ложится на Frontend-разработчика. Это нестандартная задача для значительной части фронтендеров, поскольку требует знаний, выходящих за рамки разработки браузерного приложения. Но наша команда разобралась, как настраивать и проводить мониторинг — и мы готовы поделиться опытом! 🙂
Какие инструменты использовать
Интегральный мониторинг приложения можно проводить с помощью разных сервисов, но наша команда предпочитает работать с Sentry, Grafana, Prometheus и Elastic APM. Разберем подробнее, что из себя представляет каждый инструмент.
Sentry — это программа с открытым исходным кодом, предназначенная для отслеживания ошибок. Она показывает все сбои в стеке по мере их возникновения и предоставляет данные, которые помогут их исправить.
В процессе развития к сервису добавилась трассировка, которая позволяет оценивать производительность приложений. Однако мы этот инструмент используем именно для первоначальной задачи.
Grafana — это мультиплатформенное веб-приложение для аналитики и интерактивной визуализации с открытым исходным кодом. Оно предоставляет диаграммы, графики и оповещения при подключении к поддерживаемым источникам данных.
Prometheus — это система сбора и хранения метрик в формате временных рядов, а также система оповещения об инцидентах. Prometheus предоставляет доступ к метрикам через web-интерфейс.
Grafana и Prometheus обычно используются в связке, поскольку перед тем, как отобразить данные, их нужно куда-то собрать. Как раз сервис Prometheus аккумулирует данные и передает их в Grafana. Хотя мы используем и другие источники данных: например, тот же Sentry (для мониторинга общего количества ошибок на фронтенде) и Kibana (для мониторинга событий).
3. Elastic APM — это мониторинг производительности приложений, который способен решать различные задачи. Наша команда чаще всего использует только одну его фичу — сквозную трассировку, которая позволяет отслеживать, как составляющие сервиса передают друг другу запросы из бэкенда, фронтенда и SSR, и сколько времени на это уходит.
На какие метрики обращать внимание
В первую очередь стоит заняться настройкой четырех «золотых сигналов SRE». Рассмотрим каждый подробнее.
Задержка показывает, сколько времени занимает обработка запроса. Например, сколько может пройти от момента выдачи в браузер до конца рендера или от начала запроса пользователя в адресной строке до момента загрузки приложения.
Трафик показывает, сколько запросов и транзакций происходит в единицу времени в сервисе.
Ошибки могут подсказать, с какими конкретно запросами есть проблема.
В случае с последней метрикой важно помнить, что на фронтенде всегда будут ошибки, если у вас высокий трафик — поскольку есть большая вариативность клиентов и негарантированный канал связи. И имеет смысл мониторить не наличие ошибок вообще и не абсолютное их количество, а относительное.
4. Насыщенность говорит о том, насколько вы близки к полной загрузке сервиса: то есть, как долго сервис сможет оставаться жизнеспособным, если трафик сильно вырастет.
Эта метрика не нужна, если в приложении нет бэкенда. Однако если есть SSR или бэкенд-код, мониторить ее состояние стоит.
В дашборде важно всегда учитывать ключевые метрики жизнеспособности продукта: не только загрузку сервера, статусы кодов ответа, ошибки на бэкенде и фронтенде, трафик и задержки, но и любые другие метрики, которые помогают убедиться, что пользователи не испытывают проблем при использовании продукта. Например, если вы разрабатываете сложную форму, где важно учитывать конверсию (заполнение формы и ее отправка через кнопку), то следует учесть такую метрику в мониторинге.
Чтобы вовремя реагировать на критические моменты, следует определить условия для отправки оповещений ответственной за продукт команде. Они должны учитывать только самые ключевые проблемы, которые могут возникнуть в приложении. Если оповещений станет слишком много, то они превратятся в «шум», за которым можно пропустить действительно критическую ситуацию.
Как понять, где проблема
Мониторинг позволяет выгружать показатели «золотых сигналов» и других метрик с определенной частотой. Средние значения метрик лучше не считать — будет ничего не понятно (особенно, когда происходят выбросы).
Чем детальнее метрика, тем яснее картина. А увидеть значительные проблемы позволяют показатели перцентильные (описывают частоту большинства значений) и медианные (описывают типичные значения).
Давайте посмотрим, как это выглядит в Grafana. Вот, например, перцентильные значения для распределения времени отклика страницы: P50, P90, P99. P50 — это медиана, P90 и P99 — это 90-й и 99-й перцентили.
Видно, что у половины пользователей задержка составляет менее 281 миллисекунды. Для 90% пользователей задержка меньше или равна 725 миллисекундам, для red (99%) — менее 1 секунды. Метрика отклика страницы в диапазоне P90 находятся в приемлемом значении: значит, пользователи приложения не испытывают проблем с загрузкой страницы.
Как мониторинг помогает справляться с трудностями
В прошлом году наша команда запустила новый продукт — интерактивный учебник, для написания которого использовался фреймворк Next.js. Под этот продукт мы также настроили мониторинг. Когда все было готово, увидели в Grafana, что у 90% пользователей (P90) некоторые страницы учебника открываются с задержкой более двух секунд. Это высокое значение для довольно статичного контента. Поэтому мы стали выяснять, в чем проблема.
На графике за последние пять минут мы увидели другую картину: у большинства юзеров страница открывается в течение 1 секунды.
То же было и в Elastic. Однако 95 перцентиль (отображен на графике ниже) показал, что все же задержка отдачи страницы — меньше или равна 2 секундам.
Нам нужно было срочно это исправить: согласно исследованию Google, если пользователь ждет загрузки страницы 1–3 секунды, вероятность его ухода с нее возрастает до 32%.
Мы сделали сквозную трассировку в Elastic: посмотрели, как себя вели запросы с точки зрения всей системы в целом. Эта функция и помогла разобраться в проблеме — оказалось, что запрос из бэкенда проходил через внешний контур инфраструктуры. То есть, уходил в интернет, затем заново проходил через все слои: WAF, балансировщики, роутинг и т.д., что занимало много времени.
Бэкенд-часть нашего SSR-приложения и сервис, к которому шло обращение, находились в одной внутренней сети. Поэтому связь между ними можно и нужно организовать напрямую. Мы воспользовались Service Mesh для связи сервисов. После проведенной оптимизации проблему с загрузкой долгой страницы удалось устранить: значение P90 уменьшилось и составило меньше 0,5 секунды вместо 1 секунды.
Выводы
Выстраивая процесс мониторинга для сервиса, необходимо учитывать все уровни потенциального возникновения ошибок — от серверных до продуктовых. Для успешного и полного мониторинга вашего сервиса важно:
Определить ключевые метрики жизнеспособности и SLA, чтобы у вас было только необходимое количество данных.
Не усреднять значения метрик, использовать перцентили для полноты картины.
Настроить минимум уведомлений, добавить чуть больше информации в дашборде и оставить исчерпывающую картину в других сервисах мониторинга.
Это поможет точно не пропустить критические проблемы — или убедиться, что с вашим продуктом все в порядке и пользователь не испытывает трудностей при взаимодействии с ним.
Присоединяйся к команде Учи.ру, если хочешь развивать школьный EdTech вместе с нами!