Техника «Blur up» или техника размытия фоновых изображений при загрузке

В данной статье мы расскажем о фильтрах CSS, которые вместе с такими эффектами, как режимы наложения, открывают новые возможности для обработки фоновых изображений. Также рассмотрим функцию фильтров, а также технику с использованием SVG.

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

Это особенно остро ощущается при медленном интернет-соединении, когда во время загрузки изображения перед вами отображается пустое серое поле.

В идеале изображение должно быть отослало еще в первом ответе API приложению при получении данных о профиле. Но, чтобы поместиться в этом запросе, изображение не должно весить более 200 байт. Достичь этого нелегко, так как такие фотографии обычно весят около 100 Кб.

Как сделать так, чтобы фото весило 200 байт, и что вывести на экран, пока изображение грузится?

Одним из лучших решений было бы вернуть крошечное изображение (около 40 пикселей в ширину), а затем увеличить это изображение на всю рамку с размытием по Гауссу. Таким образом, сразу виден фон, который дает кое-какое представление о будущем фоновом рисунке. Исходное изображение обложки затем может быть загружено в фоновом режиме за короткое время и заменено.

Есть несколько интересных отличий этой техники:

  • Она позволяет пользователю «воспринимать» загрузку значительно быстрее;
  • Она использует традиционные методы повышения производительности;
  • Она работает через браузер в интернете.

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

Рабочий пример

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

После загрузки это будет выглядеть примерно следующим образом:

Рабочий пример

Наиболее популярное решение в случае, если изображение используется в качестве фонового – это применение техники изменения размера (значения cover и contain). А если изображение используется как часть контента, то наиболее распространенным решением является применение новых свойств, как object-fit, которые упрощают его реализацию.

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

Вот схема того, как она работает:

  1. В теге style содержится крошечное изображение (40 на 22 пикселя), закодированное с помощью base64. Тег style также включает в себя базовые стили и правила для размытия по Гауссу для фонового изображения. Также он включает в себя стили для большого заглавного изображения на сайте;
  2. Через встроенный CSS код вы получаете URL большого изображения, которое загружается с помощью JavaScript. Если скрипт не справится, не страшно - размытое фоновое изображение и так выглядит неплохо;
  3. Когда большое изображение загружено, добавляется имя класса, при этом размытое изображение заменяется на большое.

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

Маленькое оптимизированное изображение

Сначала нужно создать предварительную версию изображения. Если изображение с размером 40 на 22 пикселя обработать с помощью программы для оптимизации изображений, то его вес уменьшится до 1000 байт:

Маленькое оптимизированное изображение

Полноразмерное JPEG изображение в 1500 на 823 пикселя весит около 120 Кб. Этот размер файла может быть намного меньше, но мы оставим его как есть, используя в качестве доказательства нашей концепции.

Функция filter () для изображений

Далее мы увеличим крошечное изображение до размеров рамки, но так, чтобы оно выглядело более качественно. Вот где может пригодиться функция filter (). Фильтры в CSS могут показаться немного запутанными, их существует три вида: свойство filter, backdrop-filter (спецификация Filter Effects Level 2) и функция filter() для изображений.

Давайте взглянем сначала на свойство filter:

.myThing {
  filter: hue-rotate(45deg);
}

С его помощью можно применить один или несколько фильтров, каждый из которых работает с результатом предыдущего. Существует целый ряд фильтров, которые мы можем использовать: blur(), brightness(), contrast(), drop-shadow(), grayscale(), hue-rotate(), invert(), opacity(), sepia() и saturate().

Хорошо еще то, что спецификация у CSS и SVG общая, то есть мы можем использовать не только заготовленные заранее фильтры, но и создавать собственные фильтры в SVG, а потом прописать их в CSS:

.myThing {
  filter: url(myfilter.svg#myCustomFilter);
}

Те же фильтры доступны при использовании backdrop-filter.

И наконец, функция filter (). Идея заключается в том, чтобы пронести все указанные ссылки на изображения в CSS через набор фильтров. А наше крошечное заглавное изображение мы встроим с помощью base64 dataURI и запустим его через фильтр blur ():

.post-header {

  background-image: filter(url(data:image/jpeg;base64,/9j/4AAQ ...[truncated] ...), blur(20px));
}

Свойство filter поддерживается в последних версиях всех браузеров, кроме IE, но ни один из них, за исключением WebKit, не реализовал спецификацию функции filter().

Когда я говорю о WebKit, я имею в виду сборки WebKit браузеров, кроме Safari. Функция filter() работает в iOS9, если добавить вендорный префикс – webkit-filter (), но об этом нигде не сообщалось официально.

Причина этого кроется в том, что браузер имеет ужасный баг со свойством background-size: исходное изображение не меняется в размере, а прошедшее через фильтр меняется. Этот баг нарушает функциональность фоновых изображений, особенно при их размытии. Он был исправлен после выпуска Safari 9, так что я предполагаю, что они не захотели объявлять это свойство.

А что нам остается делать с неисправной функцией filter ()? Можно задать обычный фон, пока изображение загрузится в браузерах, где эта функция не работает. Но если JS не удается загрузить, то и фон не загрузится.

Но мы можем использовать функцию filter () в качестве дополнительного средства для анимации переходного изображения (от размытого к четкому) вместо того, чтобы применять функцию фильтра к исходному изображению с помощью SVG.

Воссоздание фильтра с эффектом размытия с помощью SVG

В спецификации есть SVG эквивалент фильтра blur (), поэтому мы можем переделать эффект размытия в SVG с помощью нескольких хитростей:

  • При использовании размытия по Гауссу края изображения становятся немного прозрачными. Мы можем подкорректировать это, добавив фильтр feComponentTransfer. Это позволит манипулировать каждым цветовым каналом исходного изображения (включая канал альфа). Этот вариант использует feFuncA элемент, который заменяет любые значения между 0 и 1 в альфа канале на 1, вследствие чего изображение становится непрозрачным;
  • Атрибут color-interplator-filters элемента <filter> должен иметь значение sRGB. SVG фильтры по умолчанию используют цветовое пространство linearRGB, а CSS работает в sRGB. В большинстве браузеров цветовая коррекция работает правильно, но в Safari/WebKit все цвета отображаются блеклыми оттенками, если это значение не задано;
  • Атрибуту filterUnits задается значение userSpaceOnUse. Это значит, что координаты и параметры (такие как stdDeviation для размытия) действуют на определенные пиксели изображения, которое мы хотим размыть.

В результате мы получаем следующий код SVG:

<filter id="blur" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
    <feGaussianBlur stdDeviation="20" edgeMode="duplicate" />
    <feComponentTransfer>
      <feFuncA type="discrete" tableValues="1 1" />
    </feComponentTransfer>
</filter>

Свойство filter использует свою собственную url () функцию, в которой можно указать ссылку или закодированный адрес SVG фильтра. Так как же применить фильтр к содержимому свойства background-image: url (…)?

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

Это невозможно осуществить с большими изображениями, но с маленькими вполне реально. Код SVG будет выглядеть следующим образом:

<svg xmlns="http://www.w3.org/2000/svg"
     xmlns:xlink="http://www.w3.org/1999/xlink"
     width="1500" height="823"
     viewBox="0 0 1500 823">
  <filter id="blur" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
    <feGaussianBlur stdDeviation="20 20" edgeMode="duplicate" />
    <feComponentTransfer>
      <feFuncA type="discrete" tableValues="1 1" />
    </feComponentTransfer>
  </filter>
  <image filter="url(#blur)"
         xlink:href="data:image/jpeg;base64,/9j/4AAQSkZJ ...[truncated]..."
         x="0" y="0"
         height="100%" width="100%"/>
</svg>

Еще один недостаток по сравнению с использованием функции filter () для растрового изображения состоит в том, что нужно вручную задавать размеры SVG для его правильной работы с фоновым изображением. В самом SVG есть параметры viewBox для поддержания соотношения сторон изображения, и свойствам width и height задаются соответствующие значения, чтобы обеспечить вывод изображения другими браузерами. И наконец, элемент image задается, чтобы покрыть все полотно SVG.

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

Воссоздание фильтра с эффектом размытия с помощью SVG

Чтобы избежать дополнительных запросов, мы поместим SVG-обертку во встроенный в разметку код CSS. SVG необходим для кодирования URI, поэтому я использовал SVG encoder от yoksel. Теперь у нас есть dataURI, который содержит другой dataURI.

После кодировки SVG нам нужно вставить текст в url (), и чтобы техника сработала, необходимо отобразить некоторые метаданные: data:image/svg+xml; charset=utf-8,. Элемент charset очень важен: благодаря ему закодированный SVG будет правильно работать во всех браузерах:

.post-header {
  background-color: #567DA7;
  background-size: cover;
  background-image: url(data:image/svg+xml;charset=utf-8,%3Csvg...);
}

К этому моменту вся страница, включая изображение, весит 5Кб и имеет всего один запрос, если использовать GZIP.

Как получить URL большого изображения

Затем мы создаем правило для заголовка, в котором мы устанавливаем большое фоновое изображение:

.post-header-enhanced {
  background-image: url(largeimg.jpg);
}

Сначала мы загрузим большое изображение и только потом поменяем имя класса. Так мы сможем сделать плавный переход от одного изображения к другому, сначала убедившись, что большое изображение загружено. Чтобы не усложнять URL изображения ни в CSS, ни в JavaScript, мы вытянем URL с помощью JavaScript прямо из стилей. А так как имя класса мы еще не задали, то мы не можем просто использовать команду headerElement.style.backgroundImage и т.д. – о фоновом изображении им еще не известно. Чтобы решить эту проблему, мы воспользуемся CSSOM, т.е. CSS Object Model, и применим только те свойства JS, которые позволят нам преодолеть правила CSS.

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

<script>
window.onload = function loadStuff() {
  var win, doc, img, header, enhancedClass;

  // Выводим раньше, если используется старый браузер (например, IE8).
  if (!('addEventListener' in window)) {
    return;
  }

  win = window;
  doc = win.document;
  img = new Image();
  header = doc.querySelector('.post-header');
  enhancedClass = 'post-header-enhanced';

  // URL для расширенного заголовка, даже если стили не применяются.
  var bigSrc = (function () {
    // Находим все объекты CssRule внутри встроенного в разметку CSS.
    var styles = doc.querySelector('style').sheet.cssRules;
    // Извлекаем задекларированные фоновые изображения...
    var bgDecl = (function () {
      // ...с помощью функций, запущенных в цикле
      var bgStyle, i, l = styles.length;
      for (i=0; i<l; i++) {
        // ...проверяем, направленно ли действие правила на расширенный заголовок.
        if (styles[i].selectorText &&
            styles[i].selectorText == '.'+enhancedClass) {
          // Если да, то применяем bgDecl ко всем фоновым изображениям
          // значение этого правила
          bgStyle = styles[i].style.backgroundImage;
          // ...выходим из цикла.
          break; 
        }
      }
      // ... возвращаем этот текст.
      return bgStyle;
    }());
    // В конце возвращаем URL для внутреннего изображения
    // с помощью регулярного выражения перебираем все значения и присваиваем значение bgDecl всем переменным.         
    return bgDecl && bgDecl.match(/(?:(['|"]?)(.*?)(?:['|"]?))/)[1];
  }());

  // Назначаем обработчик события onLoad для превью изображения перед указанием SRC   //полновесного рисунка
  img.onload = function () {
    header.className += ' ' +enhancedClass;
  };
  // Начинаем предварительную загрузку и указываем для полновесного изображения SRC.
  if (bigSrc) {
    img.src = bigSrc;
  }
};
</script>

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

Как сделать анимацию переключения с одной картинки на другую

Узнав о существовании функции filter (), мы не стали сразу ее применять. Поэтому теперь нам нужно добавить анимированный переход от размытого изображения к более четкому. В данный момент этот способ работает только в WebKit nightlies браузерах, и поэтому можно смело использовать правило @supports.

Обратите внимание на то, что мы не можем использовать свойство transition: функцию filter () можно анимировать, но только для изменяющихся значений в цепочке фильтра. Когда фоновое изображение меняется, этот способ не работает.

Мы можем использовать анимацию, но это значит, что нам придется использовать URL фонового изображения еще два раза, как начальное и конечное значение. Ниже приведены CSS стили для четкого изображения заголовка для браузеров, которые поддерживают функцию filter ():

@supports (background-image: filter(url('i.jpg'), blur(1px))) {
  .post-header {
    transform: translateZ(0);
  }
  .post-header-enhanced {
    animation: sharpen .5s both;
  }
  @keyframes sharpen {
    from {
      background-image: filter(largeimg.jpg), blur(20px));
    }
    to {
      background-image: filter(largeimg.jpg), blur(0px));
    }
  }
}

И последняя деталь – translateZ (0) – «уловка для заголовка»: без нее анимация двигается резкими толчками. Я хотел использовать более современные средства, например, will-change:background-image, но браузер не захотел создавать отдельный аппаратный слой, так что пришлось воспользоваться старым трюком с нулевой 3D трансформацией.

Быстрые, прогрессивные фоновые изображения

Что задумали – то и получили: страницу с огромным фоновым изображением (хотя и размытым) весом в 5 Кб и медленную загрузку изображения высокого качества. На данный момент только WebKit браузеры могут анимировать переход от размытого изображения к более четкому. Но я надеюсь, что вскоре и другие браузеры внедрят функцию filter ().

РедакцияПеревод статьи «The “Blur Up” Technique for Loading Background Images»