Cколько опций должно быть в переключателе темы?
Anonymous Poll
63%
Три (системная, светлая и тёмная)
24%
Две (системная и противоположная)
8%
Ноль (системная, определяется автоматически)
5%
Смотреть ответы
Не стоит полагаться на User Agent
User Agent sniffing — это техника поиска в строке User Agent названия браузера и его версии для определения того, с какого браузера пришёл пользователь. Это можно использовать во благо для оптимизаций и загрузки полифилов.
На практике это применялось для трекинга и фингерпринтинга. Поэтому браузеры уже давно пихают в User Agent всё подряд и врут. Поэтому User Agent sniffing — крайне ненадёжная и нестабильная штука. На днях я столкнулся с этим на практике.
При аудите производительности сайта во вкладке Performance я заметил много операций принудительной перекомпоновки (forced reflow). Это когда чтение и запись геометрии элементов в JS приводит к пересчёту стилей.
Виновником стал скрипт
Интересно то, что отладка велась в Chrome версии 151, а поддержка
Этот полифил не исключение. На страницу встроен скрипт, в котором есть функция проверки перед загрузкой полифила. Также в самом полифиле функция дублируется. То есть проверка выполняется даже дважды. Вот код этой функции:
В нём проверяется версия Chrome, Safari и Firefox на основе строки User Agent. Скрипт должен подключаться, если версия Chrome меньше 126. Но он подключается и работает в Chrome 151. Где-то функция дала сбой и вернула
Выяснилось, что в моей строке User Agent не было ни Chrome, ни Firefox. Логика прошла все проверки, нигде не случился early return и в итоге дошло до
В теории User Agent выглядит как источник данных о браузере и версии. Может показаться вполне разумным брать оттуда информацию для активации полифила. Ведь известны браузеры и версии, где функция поддерживается.
На деле же строка User Agent максимально непредсказуемая, браузеры постоянно её меняют и могут врать, что и произошло в моём случае. В строке User Agent в Chrome не было ни намёка на Chrome. Браузеры делают это специально.
При разработке полифилов надёжнее будет полагаться на обнаружение функций (feature detection) — проверку фактического наличия и работоспособности функции в браузере. Это и должно служить основанием для подключения полифила.
Авторы полифила это уже поняли, поэтому открыт PR c заменой User Agent sniffing на обнаружение функций. Создаётся реальное изображение, ему задаются CSS-свойства, устанавливается атрибут и производится проверка вычисляемого значения.
Этот способ намного надёжнее, хотя не всегда возможен. Есть функции, поддержку которых нельзя или сложно проверить таким образом. Тем не менее, во многих случаях это гораздо лучше проверки на основе User Agent.
#js #performance
User Agent sniffing — это техника поиска в строке User Agent названия браузера и его версии для определения того, с какого браузера пришёл пользователь. Это можно использовать во благо для оптимизаций и загрузки полифилов.
На практике это применялось для трекинга и фингерпринтинга. Поэтому браузеры уже давно пихают в User Agent всё подряд и врут. Поэтому User Agent sniffing — крайне ненадёжная и нестабильная штука. На днях я столкнулся с этим на практике.
При аудите производительности сайта во вкладке Performance я заметил много операций принудительной перекомпоновки (forced reflow). Это когда чтение и запись геометрии элементов в JS приводит к пересчёту стилей.
Виновником стал скрипт
autosizes. Это полифил атрибута sizes="auto" у <img> и <source>. Он запрашивает размеры через getBoundingClientRect(), что и приводит к принудительным перекомпоновкам. Сам скрипт подключается автоматически.Интересно то, что отладка велась в Chrome версии 151, а поддержка
sizes="auto" реализована ещё в версии 126. То есть полифил выполняет работу в браузере, где функция поддерживается. Обычно в таких случаях логика не отрабатывает.Этот полифил не исключение. На страницу встроен скрипт, в котором есть функция проверки перед загрузкой полифила. Также в самом полифиле функция дублируется. То есть проверка выполняется даже дважды. Вот код этой функции:
const polyfillAutoSizes = () => {
if (!window.PerformanceObserver?.supportedEntryTypes?.includes("paint")) {
return false
}
const userAgent = navigator.userAgent;
const platform = navigator.platform;
const maxTouchPoints = navigator.maxTouchPoints
|| 0;
const isIOS = /iPad|iPhone|iPod/.test(platform)
|| platform === "MacIntel"
&& maxTouchPoints > 1;
const isMacSafari = platform.indexOf("Mac") === 0
&& /Safari/.test(userAgent)
&& !/Chrome|Chromium|CriOS|FxiOS|Edg|OPR|Android/.test(userAgent);
const isAppleSafari = isIOS || isMacSafari;
const browserMajorVersion = pattern => {
const match = userAgent.match(pattern);
return match
? parseInt(match[1], 10)
: null
}
const chromeVersion = browserMajorVersion(
/Chrome\/(\d+)/
);
if (chromeVersion !== null) {
return chromeVersion < 126
}
const safariVersion = isAppleSafari
? browserMajorVersion(
/Version\/(\d+).*Safari\//
)
: null;
if (safariVersion !== null) {
return safariVersion < 27
}
const firefoxVersion = browserMajorVersion(
/Firefox\/(\d+)/
);
if (firefoxVersion !== null) {
return firefoxVersion < 150
}
return true
}
;
if (!polyfillAutoSizes()) {
return
}В нём проверяется версия Chrome, Safari и Firefox на основе строки User Agent. Скрипт должен подключаться, если версия Chrome меньше 126. Но он подключается и работает в Chrome 151. Где-то функция дала сбой и вернула
true.Выяснилось, что в моей строке User Agent не было ни Chrome, ни Firefox. Логика прошла все проверки, нигде не случился early return и в итоге дошло до
return true. Вот такая у меня строка User Agent, где ни Chrome, ни его версии:Mozilla/5.0 (iPhone; CPU iPhone OS 18_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.5 Mobile/15E148 Safari/604.1
В теории User Agent выглядит как источник данных о браузере и версии. Может показаться вполне разумным брать оттуда информацию для активации полифила. Ведь известны браузеры и версии, где функция поддерживается.
На деле же строка User Agent максимально непредсказуемая, браузеры постоянно её меняют и могут врать, что и произошло в моём случае. В строке User Agent в Chrome не было ни намёка на Chrome. Браузеры делают это специально.
При разработке полифилов надёжнее будет полагаться на обнаружение функций (feature detection) — проверку фактического наличия и работоспособности функции в браузере. Это и должно служить основанием для подключения полифила.
Авторы полифила это уже поняли, поэтому открыт PR c заменой User Agent sniffing на обнаружение функций. Создаётся реальное изображение, ему задаются CSS-свойства, устанавливается атрибут и производится проверка вычисляемого значения.
const polyfillAutoSizes = () => {
// Avoid polyfilling if the browser is too old and doesn't support performance observer and paint timing
if (
!('PerformanceObserver' in window)
|| !PerformanceObserver
.supportedEntryTypes
.includes('paint')
) {
return false;
}
// Check UA styles for expected styles to support sizes auto.
const img = document.createElement('img');
img.style.setProperty(
'display',
'none',
'important'
);
img.style.setProperty(
'contain',
'revert',
'important'
);
img.sizes = 'auto';
document.documentElement.append(img);
const {contain} = getComputedStyle(img);
img.remove();
return contain !== 'size';
};
if (!polyfillAutoSizes()) {
return;
}Этот способ намного надёжнее, хотя не всегда возможен. Есть функции, поддержку которых нельзя или сложно проверить таким образом. Тем не менее, во многих случаях это гораздо лучше проверки на основе User Agent.
#js #performance
👍11🤔2🤝1
@scope и потоковая передача HTMLНоам Розенталь поделился интересной техникой, в которой совмещена потоковая передача HTML с относительно новой директивой
@scope. Её суть в том, чтобы скрыть потоковый контент до тех пор, пока он не будет полностью получен.Суть потоковой передачи в том, что HTML отправляется с сервера фрагментами по мере готовности, а браузер принимает эти фрагменты и рисует интерфейс. То есть можно показывать по одной карточке в каталоге по мере их загрузки.
Директива
@scope ограничивает область видимости стилей. При её размещении в HTML внутри элемента <style> без указания селектора, область видимости ограничивается ближайшим родителем элемента <style>.Про это подход, который Крис Койер назвал «DOM Blasters», на канале есть пост. Совместив
@scope с потоковой передачей HTML получится скрывать контент до полной готовности с помощью CSS.<section>
<style>
@scope {
opacity: 0;
}
</style>
<!-- потоковый контент -->
<style>
@scope {
opacity: 1;
}
</style>
</section>
Как это работает: сервер присылает фрагмент с открывающим
<section> и <style> с opacity: 0. Благодаря @scope стили применятся к ближайшему предку, то есть к <section>. Прозрачность будет применена и весь раздел не будет виден.Затем с сервера передаются фрагменты контента в потоковом режиме. Пока это происходит, раздел остаётся прозрачным. После передачи контента досылается ещё один фрагмент со
<style> с opacity: 1 и закрывающим </section>.Так как
<style> использует тот же приём со @scope и находится в DOM после первого <style>, то он его переопределяет. В итоге после окончания потоковой передачи всего раздела прозрачность отменяется и раздел становится видимым.Вместо
opacity можно применить другие свойства, а также добавить переходы и анимацию. Тогда раздел может плавно появляться после окончания потоковой передачи контента. Ограничивает только поддержка @scope.#html #css
👍9🤝1
Processing Instructions и маркеры
В DOM всё представлено в виде узлов (Node) разных типов. Получить тип можно через свойство
Данный тип узла представлен в HTML открывающим тегом
Это инструкции для парсера, на которые он как-то реагирует, если поддерживает их, или игнорирует, если не поддерживает. Кто работал с XML и XHTML, могут помнить, как с их помощью указывалась версия XML и подключались внешние стили:
До недавних пор processing instructions обрабатывались парсером HTML как обычные комментарии и игнорировались. Всё изменилось с добавлением в Chrome новой функции под названием Declarative Partial Updates.
В Chrome 148 с флагом экспериментальных функций доступны инструкции для парсера:
-
-
-
Инструкция
При нажатии на кнопку «Показать ещё» можно через
Сервер отвечает фрагментом HTML с элементом
Новый фрагмент может также возвращать маркер для реализации бесконечной подгрузки. В качестве альтернативы
Суть у них та же — создание области для подстановки динамического контента. Разница в том, что между
Прежде всего маркеры предназначены для потоковой передачи HTML, чтобы была возможность досылать фрагменты в произвольном порядке, но помещать их в нужные места. Но варианты использования не ограничиваются потоковой передачей.
Не смотря на поддержку, Shopify уже экспериментирует с этим API для динамического обновления корзины, количества товаров в корзине, товарной сетки, фильтров и так далее. Для API доступен полифил.
#html #web_api
В DOM всё представлено в виде узлов (Node) разных типов. Получить тип можно через свойство
nodeType. Всего в DOM на данный момент 9 типов узлов, один из которых — PROCESSING_INSTRUCTIONS. Он существует, но не используется.Данный тип узла представлен в HTML открывающим тегом
<?, за которым идёт название инструкции, затем произвольное количество атрибутов, как у обычных HTML-элементов и закрывающим тегом ?>. Вот так выглядит синтаксис:<?instruction attribute="value"?>
Это инструкции для парсера, на которые он как-то реагирует, если поддерживает их, или игнорирует, если не поддерживает. Кто работал с XML и XHTML, могут помнить, как с их помощью указывалась версия XML и подключались внешние стили:
<?xml version="1.0" encoding="UTF-8"?>
<?xml-stylesheet href="styles.css"?>
<!-- В HTML это эквивалентно комментариям -->
<!--xml version="1.0" encoding="UTF-8"-->
<!--xml-stylesheet href="styles.css"-->
До недавних пор processing instructions обрабатывались парсером HTML как обычные комментарии и игнорировались. Всё изменилось с добавлением в Chrome новой функции под названием Declarative Partial Updates.
В Chrome 148 с флагом экспериментальных функций доступны инструкции для парсера:
-
<?marker name="..."?>-
<?start name="..."?>-
<?end?>Инструкция
<?marker name="..."?> создаёт именованную область в DOM, на место которой подставляется добавленный контент из <template> с атрибутом for, значение которого совпадает с атрибутом name у маркера.<ul>
<li>
<article>
<!-- карточка 1 -->
</article>
</li>
<li>
<article>
<!-- карточка 2 -->
</article>
</li>
<?marker name="cards"?>
</ul>
<button type="button">
Показать ещё
</button>
При нажатии на кнопку «Показать ещё» можно через
fetch() запросить у сервера фрагмент разметки с дополнительными карточками вместо JSON. Вместо полной отрисовки всей страницы сервер рисует только карточки.Сервер отвечает фрагментом HTML с элементом
<template for="cards">, в котором будет разметка дополнительных карточек. Вставка такого фрагмента внутрь элемента с маркером приведёт к переносу карточек из <template> на место маркера.<!-- ответ сервера -->
<template for="cards">
<li>
<article>
<!-- карточка 3 -->
</article>
</li>
<li>
<article>
<!-- карточка 4 -->
</article>
</li>
</template>
<!-- итоговый результат -->
<ul>
<li>
<article>
<!-- карточка 1 -->
</article>
</li>
<li>
<article>
<!-- карточка 2 -->
</article>
</li>
<li>
<article>
<!-- карточка 3 -->
</article>
</li>
<li>
<article>
<!-- карточка 4 -->
</article>
</li>
</ul>
<button type="button">
Показать ещё
</button>
Новый фрагмент может также возвращать маркер для реализации бесконечной подгрузки. В качестве альтернативы
<? marker name="..."?> можно использовать инструкции <?start name="..."?> и <?end?>, которые работают в паре.Суть у них та же — создание области для подстановки динамического контента. Разница в том, что между
start и end можно поместить контент, который будет заменён. Это пригодится для лоадеров или как в сценарии с кнопкой.<!-- изначальная разметка -->
<ul>
<li>
<article>
<!-- карточка 1 -->
</article>
</li>
<li>
<article>
<!-- карточка 2 -->
</article>
</li>
<?marker name="cards"?>
</ul>
<?start name="button"?>
<button
type="button"
data-page="2"
>
Показать ещё
</button>
<?end?>
<!-- ответ сервера -->
<template for="cards">
<li>
<article>
<!-- карточка 3 -->
</article>
</li>
<li>
<article>
<!-- карточка 4 -->
</article>
</li>
<?marker name="cards"?>
</template>
<template for="button">
<?start name="button"?>
<button
type="button"
data-page="3"
>
Показать ещё
</button>
<?end?>
</template>
<!-- итоговый результат -->
<ul>
<li>
<article>
<!-- карточка 1 -->
</article>
</li>
<li>
<article>
<!-- карточка 2 -->
</article>
</li>
<li>
<article>
<!-- карточка 3 -->
</article>
</li>
<li>
<article>
<!-- карточка 4 -->
</article>
</li>
<?marker name="cards"?>
</ul>
<?start name="button"?>
<button
type="button"
data-page="3"
>
Показать ещё
</button>
<?end?>
Прежде всего маркеры предназначены для потоковой передачи HTML, чтобы была возможность досылать фрагменты в произвольном порядке, но помещать их в нужные места. Но варианты использования не ограничиваются потоковой передачей.
Не смотря на поддержку, Shopify уже экспериментирует с этим API для динамического обновления корзины, количества товаров в корзине, товарной сетки, фильтров и так далее. Для API доступен полифил.
#html #web_api
Chrome for Developers
Declarative partial updates | Web Platform | Chrome for Developers
Learn about new out-of-order streaming capabilities and the renewed HTML insertion and streaming methods available in Chrome.
👍11🔥3🤝2❤1
Persistent Widgets
Многие сайты не должны быть SPA. Но иногда эта архитектура продиктована ограничениями. Хороший пример — сайт подкаста: статические страницы эпизодов, но есть плеер, состояние которого нужно сохранять между страницами.
Ради сохранения состояния плеера весь сайт создаётся как SPA, хотя в остальном на нём нет такой интерактивности, которая требует реактивного фреймворка. Потому что в веб-платформе нет нормальных способов решения такой задачи.
Но, кажется, способ появится. Инженеры Chromium начали прототипировать идею, предложеную группой WICG (Web Incubator Community Group) под названием Persistent Widgets. Это фрейм, который сохраняется при переходах между страницами.
При переходах между страницами DOM-дерево, состояние JS, кэш GPU, соединение WebSockets или WebRTC и состояние медиа-плеера полностью уничтожаются. Чтобы сохранить состояние, предлагается новый элемент
Элемент
При навигации между страницами внутри одного origin браузер делает следующее:
1. Не уничтожает виджет и его контекст;
2. Пока следующая страница загружается, проверяет, есть ли на ней элемент
3. Виджет и его контекст переносятся в
4. Внутри виджета выбрасывается событие
Виджеты в
В качестве виджетов можно встроить
Ко всему прочему предлагается тесная интеграция
#html
Многие сайты не должны быть SPA. Но иногда эта архитектура продиктована ограничениями. Хороший пример — сайт подкаста: статические страницы эпизодов, но есть плеер, состояние которого нужно сохранять между страницами.
Ради сохранения состояния плеера весь сайт создаётся как SPA, хотя в остальном на нём нет такой интерактивности, которая требует реактивного фреймворка. Потому что в веб-платформе нет нормальных способов решения такой задачи.
Но, кажется, способ появится. Инженеры Chromium начали прототипировать идею, предложеную группой WICG (Web Incubator Community Group) под названием Persistent Widgets. Это фрейм, который сохраняется при переходах между страницами.
Дисклеймер: это обзор раннего предложения новых функций. Синтаксис может измениться в будущем или от функций могут отказаться
При переходах между страницами DOM-дерево, состояние JS, кэш GPU, соединение WebSockets или WebRTC и состояние медиа-плеера полностью уничтожаются. Чтобы сохранить состояние, предлагается новый элемент
<persistentwidget>.<!--
Страница А:
https://example.com/index.html
(перед навигацией)
-->
<persistentwidget
id="assistant"
src="/assistant.html"
>
</persistentwidget>
<!--
Страница Б:
https://example.com/dashboard.html
(после навигации)
-->
<head>
<!--
Отрисовка заблокирована до
парсинга и обработки виджета
-->
<link
rel="expect"
href="#assistant"
blocking="render"
>
</head>
<body>
<persistentwidget
id="assistant"
src="/assistant.html"
>
</persistentwidget>
</body>
Элемент
<persistentwidget> работает как <iframe> — встраивает внешний виджет, указанный в src. Это может быть AI-чат, бот-ассистент, медиа-плеер, виджет с открытым WebSockets соединением или просто отдельная страница с контентом.При навигации между страницами внутри одного origin браузер делает следующее:
1. Не уничтожает виджет и его контекст;
2. Пока следующая страница загружается, проверяет, есть ли на ней элемент
<persistentwidget> с такими же значениями атрибутов id и src;3. Виджет и его контекст переносятся в
<persistentwidget> на следующую страницу без перезагрузки и сброса состояния JS;4. Внутри виджета выбрасывается событие
openerchange.Виджеты в
<persistentwidget> могут быть размещены только на том же origin, что и основной документ. Виджеты третьих сторон предлагается выстраивать через <iframe> в <persistentwidget>. Это одно из ограничений ради безопасности.<!-- ai-chat.html -->
<body>
<iframe
src="https://ai-chat.com/chat"
allow="microphone"
>
</iframe>
</body>
<!-- Страница «Доставка» -->
<body>
<h1>Доставка</h1>
<!-- ... -->
<persistentwidget
id="ai-chat"
src="/ai-chat.html"
>
</persistentwidget>
</body>
<!-- Страница «О нас» -->
<body>
<h1>О нас</h1>
<!-- ... -->
<persistentwidget
id="ai-chat"
src="/ai-chat.html"
>
</persistentwidget>
</body>
В качестве виджетов можно встроить
<audio> или <video> и они продолжат воспроизведение при переходе между страницами, если соблюдаются условия (same-origin, наличие <persistentwidget> с одинаковыми id и src).Ко всему прочему предлагается тесная интеграция
<persistentwidget> с BFCache и Cross-Document View Transitions, а также дополнительные JS API. Persistent Widgets находятся в стадии прототипирования и многое может измениться.#html
🔥15❤3👏1🤝1
Эволюция стилей в темах Shopify
Адам Ватан на днях поделился новостью, что Shopify выкупил Tailwind. На DotDev 2026, конференции, организованной Shopify, был анонс внедрение Tailwind в движок Shopify Storefront Renderer. В связи с этим обзор подходов к стилям в Shopify.
Вопрос организации стилей в темах Shopify лежит на плечах авторов тем. Тут каждый сам решает, как к этому подойти. Поэтому можно найти разные решения. В рамках этого поста я сосредоточусь на решениях в официальных темах от Shopify.
Тема Debut, 2016-2021 годы. Все стили находятся в файле
Файл представлен как
Сами стили написаны в синтаксисе SCSS, что отражено в виде расширения
У подхода две проблемы:
- Время компиляции откладывает загрузку страницы и TTFB, особенно при больших размерах файла и при большом количестве SCSS-фич;
- Большой файл стилей откладывает отрисовку до тех пор, пока не будет полностью загружен и обработан.
Тема Dawn, 2021-2025 годы. От компиляции Liquid и SCSS отказались. Стили находятся в файлах
Применяется сразу несколько техник загрузки стилей. Первая —
Вторая техника — отложенная загрузка. С атрибутом
Чтобы не ломать страницу, можно добавить резервный вариант внутри
Однако такая организация стилей требует грамотного разделения стилей, знания используемых техник и дополнительного менеджмента со стороны разработчика. В разметке много элементов
Тема Horizon, 2025-2026 годы. Новая флагманская тема Horizon пришла на смену Dawn с архитектурой блоков. Это независимые компоненты с настройками в админке, которые могут вставляться в секции на любой странице и друг в друга.
Это потребовало изменений в организации стилей. При старом подходе это означало бы десятки элементов
Стили размещаются в Liquid-тэге
Стили извлекаются из тэгов
Позже тэг
Другая причина внедрения
В Horizon сохраняется файл с базовыми стилями
В стилях отсутствует системный подход, много сложных селекторов с комбинаторами и высокой специфичностью, связанные стили могут находится в разных файлах, много переопределений. Всё это усложняет кастомизацию и поддержку.
Также агентам сложно работать с такой кодовой базой не смотря на набор скиллов от разработчиков темы. В ответ на все эти вызовы Shopify объявил о внедрении поддержки Tailwind, а теперь и новость о приобретении Tailwind.
То есть вместо создания CSS-файлов, использования тэгов
При всей моей нелюбви к Tailwind, выбор Shopify в пользу Tailwind понятен. Бандл будет минимальным, агентам будут проще работать, разработчикам не нужно будет думать про организацию стилей, отдельные файлы, именование и специальные тэги.
Пока это планы, реализации нет. Но всё равно интересно проследить эволюцию и увидеть, как в итоге всё больше приходят к Tailwind. Я со своими предпочтениями к ванильному CSS немного грущу, но что уж тут поделать.
#css
Адам Ватан на днях поделился новостью, что Shopify выкупил Tailwind. На DotDev 2026, конференции, организованной Shopify, был анонс внедрение Tailwind в движок Shopify Storefront Renderer. В связи с этим обзор подходов к стилям в Shopify.
Вопрос организации стилей в темах Shopify лежит на плечах авторов тем. Тут каждый сам решает, как к этому подойти. Поэтому можно найти разные решения. В рамках этого поста я сосредоточусь на решениях в официальных темах от Shopify.
Тема Debut, 2016-2021 годы. Все стили находятся в файле
theme.scss.liquid, где примерно 5700 строк кода. Файл содержит стили normalize.css, карусели slick, системы сетки, разных компонентов и шаблонов страниц самой темы.Файл представлен как
.liquid, то есть там доступны ограниченные возможности шаблонизатора Liquid. Это используется для получения различных настроек из админкм и подстановки в переменные для дальнейшего использования в стилях.Сами стили написаны в синтаксисе SCSS, что отражено в виде расширения
.scss. Бэкенд Shopify компилирует Liquid в SCSS, а затем SCSS в CSS по запросу. В коде много SCSS-переменных, миксинов, функций и кое где используется вложенность.У подхода две проблемы:
- Время компиляции откладывает загрузку страницы и TTFB, особенно при больших размерах файла и при большом количестве SCSS-фич;
- Большой файл стилей откладывает отрисовку до тех пор, пока не будет полностью загружен и обработан.
Тема Dawn, 2021-2025 годы. От компиляции Liquid и SCSS отказались. Стили находятся в файлах
.css или встроены в <style>, где требуется кастомизация. На страницу подключается набор базовых стилей и стили используемых секций.<head>
<!-- Базовые стили подключаются на всех страницах -->
<link
rel="stylesheet"
href="//shop.myshopify.com/cdn/.../base.css"
>
<!-- ... -->
</head>
<body>
<header class="section-header">
<style>
/*
Некоторые критические стили
шапки для быстрой отрисовки
*/
</style>
<!--
Менее критические стили, которые
не нужны сразу, загружаются лениво
-->
<link
rel="stylesheet"
href="//shop.myshopify.com/cdn/.../component-search.css"
media="print"
onload="this.media='all'"
>
</header>
<section
id="...__imge-banner"
class="shopify-section section"
>
<!--
Стили секции подключаются
перед контентом самой секций
-->
<link
rel="stylesheet"
href="//shop.myshopify.com/cdn/.../section-image-banner.css"
media="all"
>
<!-- Контент секции -->
</section>
<!-- Аналогично в других секциях -->
</body>
Применяется сразу несколько техник загрузки стилей. Первая —
<link> в <body>. Хотя <link> обычно находится в <head>, размещение в <body> даёт эффект последовательной отрисовки и улучшает показатель FCP за счёт уменьшения блокировок.Вторая техника — отложенная загрузка. С атрибутом
media="print" стили загружаются с пониженным приоритетом и не блокируют отрисовку. Когда они загрузятся, срабатывает обработчик onload и меняет print на all. После этого стили применяются.Чтобы не ломать страницу, можно добавить резервный вариант внутри
<noscript> без media="print" и обработчика onload. Если JS по каким-то причинам будет недоступен, стили загрузятся обычным способом.Однако такая организация стилей требует грамотного разделения стилей, знания используемых техник и дополнительного менеджмента со стороны разработчика. В разметке много элементов
<link>, которые провоцируют множество запросов.Тема Horizon, 2025-2026 годы. Новая флагманская тема Horizon пришла на смену Dawn с архитектурой блоков. Это независимые компоненты с настройками в админке, которые могут вставляться в секции на любой странице и друг в друга.
Это потребовало изменений в организации стилей. При старом подходе это означало бы десятки элементов
<link> в <body> и много запросов за небольшими файлами стилей. Это работоспособно, но не очень удобно и засоряет разметку.Стили размещаются в Liquid-тэге
stylesheet внутри файла секций и блоков. Это похоже на организацию Single File Component во Vue, когда стили колоцированы в одном файле вместе с шаблоном и логикой.<div class="block-heading">
<!-- Шаблон блока -->
</div>
{% stylesheet %}
/* Стили блока */
{% endstylesheet %}
{% schema %}
// Настройки блока
{% endschema %}
Стили извлекаются из тэгов
stylesheet у всех секций и блоков и объединяются в один бандл compiled_assets/styles.css. Это шаг назад к одному большому CSS-файлу времён Debut, но теперь он генерируется автоматически.Позже тэг
stylesheet был доработан так, чтобы для каждой страницы извлекались стили только используемых на ней секций и блоков. В теории это должно было давать бандл под каждую страницу, чего на практике не происходит.Другая причина внедрения
stylesheet — развитие агентской разработки. Агентам проще работать и сохранять контекст в рамках одного файла. Колокация шаблона и стилей этому способствует, а также уменьшает потребление токенов.В Horizon сохраняется файл с базовыми стилями
base.css и некоторое количество встроенных в <style> стилей. Что в Dawn, что в Horizon стили написаны достаточно сложно и запутано (во многом благодаря применению ИИ при разработке).В стилях отсутствует системный подход, много сложных селекторов с комбинаторами и высокой специфичностью, связанные стили могут находится в разных файлах, много переопределений. Всё это усложняет кастомизацию и поддержку.
Также агентам сложно работать с такой кодовой базой не смотря на набор скиллов от разработчиков темы. В ответ на все эти вызовы Shopify объявил о внедрении поддержки Tailwind, а теперь и новость о приобретении Tailwind.
То есть вместо создания CSS-файлов, использования тэгов
stylesheet и выстраивания системы, в шаблонах Liquid будут доступны классы Tailwind. Бэкенд Shopify будет анализировать шаблон и генерировать итоговый CSS-файл из Tailwind-классов.При всей моей нелюбви к Tailwind, выбор Shopify в пользу Tailwind понятен. Бандл будет минимальным, агентам будут проще работать, разработчикам не нужно будет думать про организацию стилей, отдельные файлы, именование и специальные тэги.
Пока это планы, реализации нет. Но всё равно интересно проследить эволюцию и увидеть, как в итоге всё больше приходят к Tailwind. Я со своими предпочтениями к ванильному CSS немного грущу, но что уж тут поделать.
#css
❤8🤝2
Подклассы Event вместо CustomEvent
Многие библиотеки предоставляют систему событий в качестве публичного API. Потребители могут подписываться на них и изменять поведение. Часто основой для событий служит класс
Нет ничего плохого в использовании
При работе с
Если событие не предполагает полезной нагрузки и служит только для оповещения, то достаточно обычного объекта
У
Если же событие подразумевает полезную нагрузку, то вместо
Также для собственных подклассов
Больше деталей про использование
- Stop Using CustomEvent;
- Creating Truly Custom Events for Web Components.
#js #web_api
Многие библиотеки предоставляют систему событий в качестве публичного API. Потребители могут подписываться на них и изменять поведение. Часто основой для событий служит класс
CustomEvent, у которого в detail могут быть данные.// Где-то в коде компонента
component.dispatchEvent(
new CustomEvent(
'quantity-change',
{
detail: {
newValue,
oldValue
}
}
)
);
Нет ничего плохого в использовании
CustomEvent. Исторически он существует, потому что для авторов библиотек нужен был механизм пользовательских событий, а наследование от встроенных классов до появления ES6 не поддерживалось.При работе с
CustomEvent нужно помнить, что полезная нагрузка события находится в свойстве detail. Нужен дополнительный шаг в цепочке доступа к свойству объекта или деструктуризация, что не очень удобно с точки зрения DX.component.addEventListener(
'quantity-change',
(e) => {
// чтение из detail
const newValue = e.detail.newValue;
const oldValue = e.detail.oldValue;
// или деструктуризация
const {
newValue,
oldValue
} = e.detail;
}
);
Если событие не предполагает полезной нагрузки и служит только для оповещения, то достаточно обычного объекта
Event. Его конструктор принимает тип (имя) и объект с параметрами события (bubbles, cancelable, composed).У
Event есть встроенные подкласы. Среди них, например, ErrorEvent, ToggleEvent или PointerEvent. Если семантика какого-то встроенного события подходит цели, то можно использовать соответствующий встроенный подкласс Event.Если же событие подразумевает полезную нагрузку, то вместо
CustomEvent можно использовать собственный подкласс Event. Так как это обычный класс, можно объявить произвольные поля экземпляра, без промежуточного detail.class QuantityChangeEvent extends Event {
static eventName = 'quantity-change';
newValue;
oldValue;
constructor(newValue, oldValue) {
super(QuantityChangeEvent.eventName);
this.newValue = newValue;
this.oldValue = oldValue;
}
}
// Где-то в коде компонента
component.dispatchEvent(
new QuantityChangeEvent(
newValue,
oldValue
)
);Также для собственных подклассов
Event с произвольными данными проще описать типы, чем для CustomEvent. Особенности типизации CustomEvent подробно описал Бартон Смит в статье Creating Strongly Typed Events for Web Components.Больше деталей про использование
Event можно найти в статьях от Джастина Фагнани и Бартона Смита:- Stop Using CustomEvent;
- Creating Truly Custom Events for Web Components.
#js #web_api
❤8👎2🤝1
Реализация пользовательских атрибутов
Под конец прошлого года я рассказывал об API пользовательских атрибутов, который предложен на замену отклонённых Apple настраиваемых встроенных элементов (Customized Built-In Elements). Это часть API веб-компонентов.
В Chrome и Firefox можно описать такой пользовательский элемент, который будет наследоваться от встроенного элемента, например кнопки, заголовка или абзаца. С помощью атрибута
Это позволяет сохранять поведение и семантику встроенных элементов HTML, но расширять их возможности и добавлять новое поведение. Apple категорически от этого отказались и Safari никогда не будет поддерживать эту возможность.
Пользовательских атрибуты предложены как альтернатива. На днях Кит Сиркель представил полифил пользовательских атрибутов, дополнив тем, что хотел бы видеть это в браузерах. А это значит, что скоро мы увидим прототип в Firefox.
Пользовательские атрибуты похожи на пользовательские элементы способом описания, регистрацией и жизненным циклом. Но с атрибутами можно расширять существующие элементы не вводя новые. Идея похожа на атрибут
В примере к полифилу приводится атрибут
Использование в HTML:
Такой API выглядит интереснее, чем
Вторая: один атрибут можно применить к разным элементам из-за наследования от
Пользовательские атрибуты дадут авторам HTML-first библиотек более удобный способ связывания атрибутов с элементами и возможность реагирования на события жизненного цикла. Кроме того, это способ расширять стандартные элементы.
Вместе с полифилом есть ссылка на эксплейнер, в котором подробнее описан предлагаемый API. Также есть страницы с описанием мотивации и дизайна API. Я заинтересован и буду ждать появления прототипа в Firefox.
#html #js #web_api
Под конец прошлого года я рассказывал об API пользовательских атрибутов, который предложен на замену отклонённых Apple настраиваемых встроенных элементов (Customized Built-In Elements). Это часть API веб-компонентов.
В Chrome и Firefox можно описать такой пользовательский элемент, который будет наследоваться от встроенного элемента, например кнопки, заголовка или абзаца. С помощью атрибута
is пользовательский элемент связывается со встроенным.Это позволяет сохранять поведение и семантику встроенных элементов HTML, но расширять их возможности и добавлять новое поведение. Apple категорически от этого отказались и Safari никогда не будет поддерживать эту возможность.
Пользовательских атрибуты предложены как альтернатива. На днях Кит Сиркель представил полифил пользовательских атрибутов, дополнив тем, что хотел бы видеть это в браузерах. А это значит, что скоро мы увидим прототип в Firefox.
Пользовательские атрибуты похожи на пользовательские элементы способом описания, регистрацией и жизненным циклом. Но с атрибутами можно расширять существующие элементы не вводя новые. Идея похожа на атрибут
is.Дисклеймер: это обзор раннего предложения новых функций. Синтаксис может измениться в будущем или от функций могут отказаться
В примере к полифилу приводится атрибут
persist-value, который вешает слушатель события input и записывает значение элемента в LocalStorage. В роли ключа используется значение атрибута. Такой атрибут применим к разным полям ввода.class PersistValue extends Attr {
connectedCallback() {
const stored = localStorage.getItem(
this.value
);
if (stored !== null) {
this.ownerElement.value = stored;
}
this
.ownerElement
.addEventListener(
"input",
this
);
}
disconnectedCallback() {
this
.ownerElement
.removeEventListener(
"input",
this
);
}
attributeChangedCallback(oldValue, newValue) {
if (oldValue !== null) {
localStorage.removeItem(oldValue);
}
}
handleEvent() {
localStorage.setItem(
this.value,
this.ownerElement.value
);
}
}
customAttributes.define(
"persist-value",
PersistValue
);Использование в HTML:
<input
name="email"
persist-value="email-draft"
>
<textarea
name="bio"
persist-value="bio-draft"
>
</textarea>
Такой API выглядит интереснее, чем
is как минимум по двум причинам. Первая: у одного элемента можно указать сразу несколько разных атрибутов. В is можно указать только имя одного конкретного пользовательского элемента.Вторая: один атрибут можно применить к разным элементам из-за наследования от
Attr. В is можно указать имя только того пользовательского элемента, который наследуется от соответствующего типа встроенного элемента.Пользовательские атрибуты дадут авторам HTML-first библиотек более удобный способ связывания атрибутов с элементами и возможность реагирования на события жизненного цикла. Кроме того, это способ расширять стандартные элементы.
Вместе с полифилом есть ссылка на эксплейнер, в котором подробнее описан предлагаемый API. Также есть страницы с описанием мотивации и дизайна API. Я заинтересован и буду ждать появления прототипа в Firefox.
#html #js #web_api
👍4