backdrop-filter: blur() — как он работает и как его правильно использовать

от автора

Наш дизайнер очень любит использовать в работе новое свойство backdrop-filter: blur(). Его можно понять. Это одно из самых эффектных свойств, благодаря которому создается эффект матового стекла.

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

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

Как работает backdrop-filter: blur()?

backdrop-filter применяет графический эффект не к самому элементу, а к тому, что визуально находится за ним. В отличие от filter, который обрабатывает содержимое элемента, backdrop-filter работает с его фоном. blur() размывает всё, что визуально видно за элементом через его прозрачность.

На примере ниже вы можете увидеть как это отображается:

Чтобы понять почему backdrop-filter так часто приводит к неожиданным эффектам, нужно вспомнить как браузер рендерит страницу.

Браузер не рисует всё в один слой. Он строит дерево compositing layers — отдельных текстур, которые GPU потом склеивает в финальную картинку. Каждый compositing layer — это независимая GPU-текстура.

Когда браузер встречает элемент с backdrop-filter, он:

  • Собирает snapshot — берёт снимок всего, что нарисовано позади элемента в радиусе blur.

  • Применяет фильтр к этому снимку.

  • Рисует элемент поверх размытого снимка.

Ключевой момент: snapshot берётся из compositing layer — уже готовой GPU-текстуры. Не из DOM-дерева, не из CSS-иерархии. Это значит, что DOM-структура — кто чей родитель, какие стоят overflow: hidden, как выстроены z-index — на это не влияет. И в этом и кроется причина его неожиданного поведения.

Почему большой blur — это большая проблема?

backdrop-filter: blur() в браузерах реализован через алгоритм Gaussian blur: каждый пиксель — это перемешение цветов всех соседних пикселей в заданном радиусе. Ближние пиксели влияют сильнее, дальние слабее — отсюда плавный, «размытый» переход.

Таким образом, для каждого пикселя результата нужно обработать все соседние пиксели в радиусе. При blur(20px) — это круг радиусом 20px. При blur(70px) — радиусом 70px.

Площадь круга растёт как π×R². Получается, если увеличили радиус в 4 раза — площадь обработки выросла в 16 раз. GPU делает в 16 раз больше работы.

При этом CSS-пиксели и физические пиксели — разные вещи. То есть когда вы пишете blur(20px) — браузер применяет его в физических пикселях:

  • Обычный монитор (1×): обрабатывается область 20px × 20px;

  • Retina (2×): обрабатывается область 100px × 100px — в 4 раза больше;

  • iPhone 15 Pro (3×): обрабатывается область 150px × 150px — в 9 раз больше.

Также важно понимать, что динамический контент под blur  вызывает постоянный перерасчёт. Если под размытым элементом находится что-то меняющееся — скролящийся список, видео, анимация — браузер не может кешировать snapshot. На каждый кадр он:

  • Выбросить старый snapshot;

  • Сделать новый snapshot;

  • Заново применить blur

Оптимизация производительности

Использование transform: translateZ(0) или will-change: backdrop-filter не уменьшает стоимость самого blur, но создаёт для элемента отдельный compositing layer. Это изолирует перерисовку: браузеру не нужно пересчитывать blur при изменениях соседних элементов — переcчитывается только слой с фильтром.

В итоге blur работает не дешевле, но переcчитывается реже, что снижает лаги и делает интерфейс плавнее.

Какие проблемы порождает неправильное использование backdrop-filter: blur()?

1. blur «вылезает» за пределы контейнера

Вот реальный сценарий, который мы поймали в проекте.

Есть scroll-контейнер с карточками внутри. Внизу — кнопка PLAY с ярким фоном.

Когда карточка доскролливается близко к нижней границе контейнера и оказывается в 70px от кнопки PLAY — её blur(70px) «видит» кнопку и  ее фон начинает светиться.

С точки зрения DOM всё правильно — кнопка снаружи контейнера с overflow: hidden. Но blur работает на уровне compositing, и не подчиняется  DOM-структуре.

Если за пределами элемента на расстоянии менее указанного у blur() есть яркий объект — он попадает в blur и окрашивает блок.

Смотрите Demo по ссылке: demo1

2. Адресная строка браузера окрашивает блок с blur

Браузеры на iPhone показывают и скрывает адресную строку при скролле. Если scroll-контейнер с карточками, имеющими backdrop-filter, доходит до края экрана — blur начинает «видеть» нативный UI браузера: серую адресную строку. И это вызывает засвечивание карточки.

Смотрите Demo по ссылке: demo2

3. Мерцание при быстром скролле

Из-за особенностей compositing в некоторых версиях Safari при прокрутке страницы backdrop-filter может на долю секунды исчезать или мигать. Это происходит в момент, когда Safari пересчитывает compositing layers при изменении scroll position.

4. Поломка из-за 3D-трансформаций на родителях

Если у родительского элемента стоит transform: translate3d() или will-change: transform, Safari может изолировать слои так, что backdrop-filter потеряет доступ к нижележащему контенту и перестанет размывать что-либо, кроме собственного фона. Особенно актуально для элементов внутри анимированных контейнеров (слайдеры, модальные окна с CSS-анимацией входа).

Как же правильно написать backdrop-filter: blur()?

overflow: hidden не помогает — это самый контринтуитивный момент. Кажется логичным: поставил overflow: hidden на родителя — дочерние элементы обрезаны, blur не выйдет за границу. Но это не так.

overflow: hidden обрезает область на уровне paint. Но backdrop-filter работает на уровне compositing и не вызывает repaint.

Напомню: Style → Layout → Paint → Composite

То же самое с isolation: isolate и transform: translateZ(0) на родителе — они создают новый stacking context и compositing layer, но в Safari/Chrome это не ограничивает область в которой браузер «читает пиксели»  дочерних элементов.

Единственный надёжный способ ограничить backdrop-filter внутри контейнера:

.scroll-container { clip-path: inset(0); /* ← это работает, overflow: hidden — нет */}

clip-path в отличие от overflow: hidden, применяется на том уровне, где работает backdrop-filter.

Если нужно скругление блока, тогда нужно использовать clip-path: inset(0 round 20px). Это физически обрезает абсолютно весь контент внутри элемента, включая любые выходящие за рамки дочерние объекты.

Также важно помнить несколько моментов для старых браузеров. Во-первых, без -webkit- префикса в старых версиях Safari эффект не применяется вообще. А также, стоит использовать fallback.

@supports not (backdrop-filter: blur(20px)) { .card {  background: rgba(30, 30, 30, 0.9); /* плотный непрозрачный фон */ }}

Браузеры без поддержки backdrop-filter покажут просто полупрозрачный фон без blur.  Без fallback на таких браузерах карточка будет почти прозрачной, а текст не читаемым.

Итого, правильный backdrop-filter: blur():

.card { /* 1. Прозрачность обязательна, иначе blur не будет виден */ background: rgba(30, 30, 30, 0.6); /* 2. Сам эффект + обязательный webkit-префикс для Safari */ backdrop-filter: blur(20px); -webkit-backdrop-filter: blur(20px); /* 3. Стабилизация слоя — убирает мерцание при скролле в Safari */ will-change: transform; } /* 4. Изоляция blur внутри контейнера — overflow: hidden не работает, нужен именно clip-path */ .scroll-container { clip-path: inset(0); /* со скруглением: clip-path: inset(0 round 20px); */ } /* 5. Fallback для браузеров без поддержки backdrop-filter */ @supports not (backdrop-filter: blur(20px)) {  .card { background: rgba(30, 30, 30, 0.9); /* плотный непрозрачный фон */  } }

ссылка на оригинал статьи https://habr.com/ru/articles/1057226/