К содержанию
/ Лаборатория
CSS-градиент

Зона C · витрина приёмов

Лаборатория

Здесь собрано то, что мы намеренно не поставили на продающую страницу. Сюда заходят по своей воле, поэтому можно позволить то, что там стоило бы конверсии. Каждый приём подписан паспортом: что это, сколько весит в байтах после gzip -9, где он уместен и где вреден.

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

01

Фон на WebGL и честная цепочка отката

Полноэкранный треугольник без вершинных буферов, фрагментный шейдер domain-warped fbm: fbm(p + 4·fbm(p + 4·fbm(p))). Реагирует на курсор и на прокрутку. Это тот самый фон, который вы сейчас видите за плитами.

ступень: CSS кадров/с: — буфер: — октав: —

    Пульт
    Приём
    Треугольник, накрывающий весь клип, рисуется одним drawArrays без единого буфера: в WebGL 2 координаты вершины считаются из gl_VertexID. Дальше вся картинка — работа фрагментного шейдера. Три уровня искажения области дают «жидкий дым», который не читается как процедурный шум.
    Вес
    Загрузчик с откатами и обработкой потери контекста — 927 байт gzip, шейдер — 563 байта. Вместе 1 490 байт. Тот же результат на three.js — 86 590 байт gzip и один внешний запрос.
    Цепочка
    WebGL 2 (4 октавы) → WebGL 1 (2 октавы, шейдер на ES 1.00) → canvas 2d (несколько дрейфующих радиальных градиентов в буфере вчетверо меньше, 30 кадров/с) → CSS-градиент (движение только на композиторе). Ступень выбирается сама; кнопкой «ступень ниже» её можно опустить руками и увидеть каждую. При prefers-reduced-motion и при update: slow берётся сразу CSS.
    Отказы
    Контекст WebGL теряется по-настоящему: переключение видеокарты на ноутбуке, сон, обновление драйвера, нехватка памяти. Кнопка «уронить контекст» вызывает WEBGL_lose_context и восстанавливает его через две секунды — видно, что страница переживает это без чёрного прямоугольника. В запросе контекста стоит failIfMajorPerformanceCaveat: без него на машине без драйверов WebGL «работает» через программный растеризатор со скоростью 3 кадра/с. Плюс замер себя самого первые полторы секунды: ниже 45 кадров/с — понижаем качество, ниже 25 — честно уходим на CSS.
    Уместен
    Фон раздела, за которым нет мелкого текста. Страница-витрина. Экран ожидания в интерфейсе, где всё равно нечего показывать.
    Вреден
    Под читаемым текстом: яркость фона меняется от кадра к кадру, и контраст вместе с ней. Здесь это решено не «затемнением сверху», а ограничением палитры самого шейдера — максимально светлый пиксель, который он может выдать, даёт с основным текстом 12,2:1, с приглушённым 5,5:1. В первом экране продающей страницы: компиляция шейдера блокирует главный поток на 5–30 мс и попадает прямо в окно измерения LCP. На телефоне в полном DPR: 2400×1080 при плотности 2,5 — это 25–60 мс на кадр, то есть 15 кадров/с и горячий корпус. Поэтому здесь на узких экранах шейдер по умолчанию не запускается — только по явному нажатию.
    02

    Трёхмерная разборка объекта по прокрутке

    Ни одной библиотеки: transform-style: preserve-3d, perspective и пять трансформаций, которые считает композитор. Объект осмысленный — это разрез сайта на слои. Прокрутите раздел: слои разъедутся и повернутся.

    Разбор раскрытие: 0 %
    Приём
    Родительский блок получает perspective, стопка — transform-style: preserve-3d, каждый слой смещается по своей оси через переменную --p (0…1), которую JS пишет один раз за кадр по положению раздела в окне. Скролл не перехватывается: страница едет как обычно, мы только читаем её положение. На широком экране добавляется наклон по курсору.
    Вес
    Чтение прогресса и закрепление — 406 байт gzip, наклон по курсору — 315 байт. Вместе 721 байт. Аналог на GSAP + ScrollTrigger — 46 266 байт и два внешних запроса.
    Уместен
    Когда у объекта действительно есть слои и их состав — часть сообщения: устройство продукта, состав работ, схема поставки. Разборка тогда объясняет, а не украшает, и человек уносит со страницы новое знание.
    Вреден
    Как украшение без смысла — это дорогой аттракцион, который смотрят один раз. На тач-экране наклон нечем задать, а сцена всё равно считается — поэтому здесь на узких экранах трёхмерная сцена просто не строится, остаётся список ниже с тем же содержанием. Внутри повёрнутых слоёв нельзя держать читаемый текст: поворот выключает субпиксельное сглаживание, и мелкий шрифт становится мыльным. Подписи слоёв поэтому вынесены наружу, а сама сцена помечена aria-hidden — скринридер читает список.
    03

    Кинетическая типографика

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

    Скорость это отказ от лишнего

    Разбивка букв: — узлов добавлено: — фаза: 0.00
    Приём
    Исходная строка лежит в разметке целиком — без JS она просто читается как заголовок. Скрипт запоминает её в aria-label, режет на слова, слова на буквы, и раз за кадр прокрутки пишет каждой букве translateY по синусу от её номера. Двигается только transform: раскладка не пересчитывается, сдвига макета нет.
    Вес
    Разбивка и волна — 318 байт gzip. Библиотечный SplitText из платного набора GSAP решает ту же задачу, но тянет за собой ядро в 28 268 байт.
    Доступность
    Каждая буква — обычный текстовый узел, поэтому выделение мышью и Ctrl+F работают. Буквы помечены aria-hidden, а на заголовке стоит aria-label с целой фразой: без этого VoiceOver и NVDA читают строку по буквам. Слова остаются неразрывными — перенос строки происходит по пробелам, как в обычном тексте.
    Уместен
    Один-два раза на всю страницу, на заголовке-высказывании, до которого человек уже добрался.
    Вреден
    В первом экране: пока буквы «собираются», текст физически нечитаем ровно в те секунды, когда человек решает, остаться ему или уйти. Плюс элемент, отвечающий за LCP, считается отрисованным позже. В основном тексте — вообще никогда. И арифметика: каждая буква — это DOM-узел и потенциальный слой композитора; на строке в 120 знаков это уже заметно на слабой машине.
    04

    Частицы-созвездие с притяжением к курсору

    Обычный canvas 2d. Ограничение 30 кадров в секунду, остановка за пределами поля зрения и при уходе вкладки в фон, буфер в пониженной плотности пикселей. Всё это — не оптимизация «на всякий случай», а условие, при котором приём вообще допустим.

    Счётчики частиц: — связей/кадр: — кадров/с: — состояние: не запущено
    Приём
    Точки летят по инерции, отражаются от краёв, тянутся к курсору в радиусе 150 пикселей. Пары ближе 118 пикселей соединяются линией, прозрачность линии — от расстояния. Рисование идёт в буфер с плотностью не выше 1,5 пикселя на CSS-пиксель.
    Вес
    549 байт gzip. Готовое решение вроде particles.js — около 25 000 байт и внешний запрос ради того же самого.
    Дисциплина
    Кадр пропускается, если с прошлого прошло меньше 33 мс — 30 кадров/с для декоративного слоя визуально неотличимы от 60, а расход вдвое меньше. IntersectionObserver полностью останавливает цикл, когда сцена ушла из окна; visibilitychange — когда ушла вкладка. Кнопка «выключить движение» оставляет один нарисованный кадр: картинка на месте, вычислений ноль.
    Уместен
    Небольшой участок фона в одном разделе, где нет текста поверх. Пустое состояние интерфейса.
    Вреден
    Сплошным фоном под текстом: линии проходят под буквами и рвут их. И арифметика связей: проверка пар растёт как квадрат числа частиц. 70 частиц — 2 415 проверок на кадр, 150 частиц — уже 11 175, и это на каждом из 30 кадров. Именно поэтому здесь число частиц считается от площади, а не задаётся «покрасивее». На узком экране сцена рисует один кадр и останавливается.
    05

    Живое сравнение весов

    Всё, что показано выше, обычно собирают на наборе из четырёх библиотек. Ниже — что это стоит в байтах после gzip -9. Замеры сделаны 20 августа 2026 года на минифицированных сборках, а не взяты из статей.

    Обычный стек three.js + GSAP + ScrollTrigger + Lenis 138 287 байт
    Ручная замена всё, что вы видите на этой странице 2 246 байт
    61×

    Разница в 61 раз по весу и четыре внешних запроса против нуля. Это не побочный эффект аккуратности — это и есть то, что мы продаём. Агентство, которое обещает скорость, обязано быть быстрым само, иначе его сайт опровергает его же предложение.

    Обычный стек, gzip -9
    Библиотекаmingzip
    three.js365 55286 590
    GSAP core72 92728 268
    ScrollTrigger44 57517 998
    Lenis18 7225 431
    Итого501 776138 287
    Ручная замена, gzip -9
    Модульrawgzip
    Загрузчик WebGL с откатами1 718927
    Шейдер domain-warped fbm1 018563
    Плавная прокрутка563364
    Закрепление по прокрутке659406
    Наклон карточек641315
    Магнитная кнопка635382
    Разбивка текста562318
    Частицы821549
    Итого без шейдера5 5992 246
    Проверка внешних запросов на этой странице: 0
    Приём
    Счётчик добегает до конечного числа за 900 мс при появлении блока в поле зрения. Конечные значения стоят в разметке сразу: без JS и при выключенном движении видны те же цифры, просто без разбега. Цифры табличные, поэтому строка не дёргается по ширине.
    Вес
    Около 180 байт gzip. Это единственный эффект из шести, который мы поставили и на продающую страницу.
    Уместен
    Там, где цифра и есть аргумент. Разбег привлекает к ней взгляд и заодно показывает масштаб разницы: одна полоса заполняет строку, вторая — полтора процента её длины.
    Вреден
    Когда цифр на экране много: несколько одновременно бегущих счётчиков превращаются в мельтешение, и ни одну цифру не удаётся прочитать. И никогда — на цене: бегущая цена читается как торг.
    Оговорка
    Честно про эту самую страницу: 2 246 байт — это вес приёмов, замеренный по модулям, а не вес витрины. Сама витрина весит 100 481 байт в исходнике и около 28,5 КБ после gzip -9: JS — примерно 12 КБ (8,6 КБ без комментариев), CSS — 5,7 КБ, остальное — текст и разметка. Точную цифру по gzip тут не привести: она меняется от самой этой строки. Разница — обвязка витрины: журнал событий, пульты, счётчики, индикатор ступени и подробные комментарии в исходнике, которые здесь часть продукта. Смысл замера не в том, что наш код невесом, а в том, что четыре библиотеки на 138 КБ дают ровно тот же результат, что 2,2 КБ ручной работы, — и добавляют четыре внешних запроса. Откройте исходный код страницы: там нечего прятать.
    06

    Выключатель движения

    Кнопка в шапке гасит всё сразу: шейдер, разборку, волну по буквам, частицы, счётчики и все переходы CSS. Проверьте — страница не потеряет ни одного слова.

    системная настройка prefers-reduced-motion: — текущее состояние: —

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

    Проба
    Приём
    Атрибут на корневом элементе плюс одно правило CSS, схлопывающее длительности анимаций и переходов; скрипты сцен смотрят на тот же флаг и просто перестают запрашивать кадры. Состояние живёт в памяти вкладки: браузерное хранилище на этом сайте не используется вовсе.
    Вес
    Около 120 байт gzip и четыре строки CSS.
    Уместен
    Везде, где движения больше, чем на одну секунду за визит. Уважение к prefers-reduced-motion — не вежливость: для части людей вестибулярные расстройства делают параллакс физически неприятным.
    Оговорка
    Выключатель не может быть оправданием для интерфейса, который без движения непонятен. Проверка простая: выключить всё и попробовать пройти сценарий. Если что-то перестало читаться — виновато не движение, а то, что смысл был спрятан в анимацию.
    07

    Что из этого попало на продающую страницу

    Почти ничего — и это не осторожность, а расчёт. Ниже список с причинами.

    Решения по каждому приёму
    ПриёмНа главнойПочему так
    Шейдер на WebGLнетКомпиляция блокирует поток в момент измерения LCP; выигрыш — только эстетический.
    Трёхмерная разборкачастичноОставлен наклон карточек SEO-слоёв: там слои — часть объяснения.
    Кинетическая типографиканетАнимация текста по буквам в первом экране прямо запрещена регламентом проекта.
    ЧастицынетДекоративная нагрузка без сообщения. Директору завода она ничего не доказывает.
    Счётчики чиселдаЦифра и есть аргумент; разбег ведёт к ней взгляд и почти ничего не стоит.
    Выключатель движениядаОбязателен везде, где есть хоть какое-то движение.

    Правило разрешения споров. Сайт агентства — это демонстрация метода. Если мы позволяем себе то, что вредит конверсии, мы продаём клиенту ровно это же. Поэтому эффектная часть вынесена сюда, на отдельную страницу, куда заходят по своей воле и где никто никого не задерживает.