От автора: в данной статье мы рассмотрим технику предварительного размытия фонового изображения во время загрузки. Уже на протяжении долгого времени нам доступны CSS фильтры. Вместе с режимами наложения с помощью фильтров нам открываются совершенно новые возможности по созданию и манипулированию объектами, которые раньше были возможны только в Photoshop’е. В данной статье Эмиль рассказывает про высокопроизводительную технику, в которой используются уже забытые фильтры – функция фильтров – а также про метод с использованием SVG.
Все началось с поста от команды разработчиков официального приложения Facebook* и того, как они загружали превью фотографий. Проблема с этими фото была в том, что они были огромные, и часто загрузка занимала довольно долгое время. А пользователь, когда видел, как монотонный фон меняется на фотографию во время загрузки, получал не самый идеальный опыт от работы с приложением.
Данная проблема стоит особенно остро на низкоскоростных или мобильных соединениях. Перед вами появляется просто пустой серый прямоугольник, и вы ждете, когда загрузится изображение.
В идеале изображение должно кодироваться еще в первом ответе от API после получения информации о профиле. Чтобы изображение поместилось в запросе, необходимо, чтобы его вес не превышал 200 байт. Проблематично, тем более, что фото весит 100 Кбайт. Так каким образом добиться того, чтобы изображение весило 200 байт, и как хоть что-либо показать пользователю до полной загрузки фотографии?
Довольно оригинальным решением будет вернуть крошечное изображение (примерно 40 пикселей в ширину) и растянуть его на весь контейнер с размытием по Гауссу. Таким образом, пользователь сразу видит более менее приемлемый фоновый рисунок и имеет представление о том, что из себя представляет настоящее изображение. Исходное изображение можно загрузить в фоновом режиме и плавно заменить. Умно! Несколько преимуществ данного метода:
Загрузка воспринимается немного быстрее
Используются традиционные методы повышения производительности.
Метод полностью работает через браузер.
Данный метод пригодится при загрузке больших фоновых изображений в хедере. Можно не загружать тяжелые изображения, но иногда необходимо искать компромисс в угоду пользователю. Самое лучшее, что можно сделать в данной ситуации, это оптимизировать восприятие производительности. Так что можете взять данный метод себе на вооружение.
Рабочий пример
Мы переделаем наш метод и воспользуемся для этого критическим CSS. Самый первый запрос будет загружать маленькое изображение и встраивать CSS код напрямую в HTML страницу, и только после предварительной прорисовки размытого изображения будет отображаться изображение высокого качества. Когда загрузка завершена, выглядеть это будет примерно так:
В этом примере изображение используется в качестве украшения, а не в качестве контента. Существуют свои тонкости в том случае, если изображение идет как контент (т.е. используется тег img) или как просто фоновое изображение. Самым распространенным решением в случае, если изображение используется в качестве фонового рисунка, будет техника умного изменения размера (как CSS значения cover и contain). В случае, когда изображение используется в качестве контента, есть новые свойства типа object-fit, облегчающие данный подход. На сайте Medium уже используется метод размытых изображений для повышения скорости загрузки, однако о полезности данного метода ведутся споры – как данный метод отреагирует, если что-то пойдет не так? Как бы то ни было, в этой статье мы сосредоточимся на методе для фоновых изображениях. Принцип работы:
Внутри тега style находится инлайновое маленькое изображение (40х22px), закодированное при помощи кодировки base64. Также внутри тега style прописываются основные стили и правила для размытия по Гауссу для фонового изображения. В другом классе описываются стили для большого изображения в хедере сайта.
Через инлайновый CSS код получается URL большого изображения, и оно загружается через JavaScript. Если по каким-либо причинам качественное изображение не загрузилось, не страшно – размытое изображение все еще на месте, смотрится неплохо.
После загрузки большого изображения добавляется специальный класс, и размытое изображение заменяется на обычное. Удаление размытого изображения можно анимировать.
Финальный пример можно взглянуть по ссылке. Прежде чем загрузится основное изображение высокого качества, мы видим размытый его вариант. Если у вас не так, перезагрузите страницу и очистите кэш.
Маленькое оптимизированное изображение
Первым делом необходимо создать превью изображения. Facebook* добился веса изображения в 200 байт с помощью сжатия (один из примеров это хранение неизменяющихся битов JPEG изображения из хедера в самом приложении), но мы не будем настолько углубляться. Если это изображение 40 на 22 пикселя пропустить через специальное программное обеспечение по оптимизации изображений, то его вес уменьшится до 1000 байт.
Большое изображение 1500 × 823 пикселей весит примерно 120 Кбайт. Размер файла может быть и меньше, но мы оставим такой. В реальной жизни легче было бы, наверное, загружать изображения разного размера в зависимости от размера окна браузера, или вообще загружать другой формат типа WebP.
Функция filter
Следующий этап это увеличить маленькое изображение до размеров контейнера таким образом, чтобы оно смотрелось приемлемо. Тут нам и поможет функция filter(). Тема фильтров в CSS довольно запутанна, существует три типа: свойство filter, backdrop-filter (спецификация Filter Effects Level 2 spec) и функция filter() для изображений. Рассмотрим сначала свойство:
1 2 3 |
.myThing { filter: hue-rotate(45deg); } |
Можно применить несколько фильтров, каждый из которых работает с результатом предыдущего – как с трансформациями. Существует целый набор заранее заданных фильтров, которые можно использовать: blur(), brightness(), contrast(), drop-shadow(), grayscale(), hue-rotate(), invert(), opacity(), sepia() и saturate().
Радует тот факт, что спецификация общая как для CSS, так и для SVG. Т.е. можно использовать не только заранее описанные фильтры, но также можно создать собственные в SVG а затем прописать их в CSS.
1 2 3 |
.myThing { filter: url(myfilter.svg#myCustomFilter); } |
При использовании backdrop-filter будет точно такой же эффект, данный метод применяется при совмещении прозрачного элемента с его фоном – лучший способ для создания эффекта «замороженного стекла».
И наконец, функция filter(). Идея в том, чтобы везде, где указаны ссылки на изображения, сначала пропустить эти изображения через набор фильтров. Наше маленькое изображение хедера мы закодируем с помощью base64 dataURI и пропустим через фильтр blur().
1 2 3 |
.post-header { background-image: filter(url(data:image/jpeg;base64,/9j/4AAQ ...[truncated] ...), blur(20px)); } |
Отлично, именно то, чего мы хотели добиться при имитировании техники приложения Facebook*. Однако возникают проблемы с поддержкой. Свойство 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-interpolation-filters тега filter должен иметь значение sRGB. SVG фильтры по умолчанию работают с цветовым пространством linearRGB, а CSS с sRGB. В большинстве браузеров цветовая коррекция работает правильно, но в Safari / WebKit браузерах без этого значения все цвета отображаются, как выцветшие.
Атрибуту filterUnits задается значение userSpaceOnUse. Простым языком, это означает что координаты и прямые (как stdDeviation для размытия) нацелены на конкретные пиксели изображения, которое необходимо размыть.
В итоге мы получаем такой SVG код:
1 2 3 4 5 6 |
<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 не могут использовать сторонние ресурсы. Но это можно обойти, закодировав JPG изображение внутри SVG при помощи base64. Для большого изображения это сделать нельзя, а вот для нашего маленького вполне возможно. Код SVG будет примерно такой:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
<svg xmlns="//www.w3.org/2000/svg" xmlns:xlink="//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 задаются соответствующие с измерениями значения (к примеру, в IE не сохраняется соотношение сторон, если данные свойства не заданы). И наконец, image элемент растягивается на все полотно SVG.
Теперь можно использовать этот файл в качестве фонового изображения в шапке сайта, выглядеть это будет примерно так:
Во избежание дополнительных запросов можно помесить блок-обертку SVG прямо в CSS. Инлайновые стили SVG нужны для кодирования URI, я использовал SVG encoder от yoksel. Теперь у нас один dataURI содержит другой dataURI. Как в фильме Начало!
После кодировки SVG в url(), необходимо вставить результат. Стоит заметить, что чтобы данный метод сработал, предварительно необходимо добавить некоторые метаданные: data:image/svg+xml;charset=utf-8,. Кодировка charset крайне важна: в случае правильной кодировки закодированный SVG будет работать во всех браузерах.
1 2 3 4 5 |
.post-header { background-color: #567DA7; background-size: cover; background-image: url(data:image/svg+xml;charset=utf-8,%3Csvg...); } |
На данный момент, если использовать GZIP, то вся страница вместе с изображением весит 5Кб и на ней всего один запрос.
Получаем URL большого изображения
Теперь необходимо создать правило для в хедере там, где мы будем заменять размытое изображение на изображение высокого качества.
1 2 3 |
.post-header-enhanced { background-image: url(largeimg.jpg); } |
Вместо того, чтобы просто поменять классы, тем самым вызывая переключение к большому изображению, мы загрузим большое изображение в фоновом режиме и только потом применим класс. Именно таким образом, если главное изображение загрузилось, мы сможем плавно переключить одно изображения на другое. Чтобы не усложнять URL картинки как в CSS, так и в JS, мы просто вытащим при помощи JavaScript URL прямо из стилей. Так как класс еще не применен, то мы можем обращаться к фоновому изображению при помощи headerElement.style.backgroundImage – стили еще не знают о фоновом изображении. Чтобы решить данную проблему мы воспользуемся CSSOM – the CSS Object Model и считаем только те JS свойства, с помощью которых можно перемещаться по CSS правилам.
Код ниже ищет класс с изображением высокого качества и при помощи регулярного выражения вытягивает из него адрес. После, изображение загружается в фоновом режиме, и как только оно загрузилось срабатывает смена класса.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 |
<script> window.onload = function loadStuff() { var win, doc, img, header, enhancedClass; // если старые браузеры, то сразу выходим (т.е. IE 8). if (!('addEventListener' in window)) { return; } win = window; doc = win.document; img = new Image(); header = doc.querySelector('.post-header'); enhancedClass = 'post-header-enhanced'; // Находим первое упоминание об адресе фонового изображения высокого //качества, даже если стили не применены. var bigSrc = (function () { // Находим все объекты CssRule внутри инлайновых стилей var styles = doc.querySelector('style').sheet.cssRules; // сохраняем объявление background-image... var bgDecl = (function () { // ...при помощи рекурсивной функции с циклом var bgStyle, i, l = styles.length; for (i=0; i<l; i++) { // ...проверяем правила на причастность к фоновому изображение хедера if (styles[i].selectorText && styles[i].selectorText == '.'+enhancedClass) { // Если да, устанавливаем bgDecl на все значение background-image bgStyle = styles[i].style.backgroundImage; // ...и прерываем цикл. break; } } // ...возвращаем текст. return bgStyle; }()); // В конце, пока переменная bgDecl имеет значение, возвращаем совпадения //по поиску URL внутри background-image при помощи регулярного выражения, //которое я нашел в интернете. return bgDecl && bgDecl.match(/(?:\(['|"]?)(.*?)(?:['|"]?\))/)[1]; }()); // Вешаем обработчик onLoad на изображение перед установкой src. img.onload = function () { header.className += ' ' +enhancedClass; }; // вызываем загрузку изображения, задав адрес. if (bigSrc) { img.src = bigSrc; } }; </script> |
Скрипт сразу завершается, если addEventListener не поддерживается. С остальными браузерами данный метод должен прекрасно работать. Насколько я знаю, все современные браузеры с поддержкой SVG также поддерживают и CSSOM с функциями JavaScript, которые использовались выше в коде.
Анимируем момент переключения
После того, как мы узнали о функции filter(), мы не стали ее сразу применять. А сейчас нам необходимо добавить анимированный переход от размытого изображения к четкому. На данный момент способ работает только в ночных сборках webkit браузеров, можно смело использовать @supports. Ниже GIF пример эффекта:
Обратите внимание на то, что мы не можем использовать свойство transition: функция filter() поддается анимации, но только для изменяющихся значений – в случае с фоновым изображением данный способ не работает. Тем не менее, можно воспользоваться анимацией, но в таком случае нам придется скопировать URL изображения еще два раза, как начальное и конечное значения. Небольшая жертва. Ниже представлены стили для четкого изображения хедера для браузеров, поддерживающих функцию filter():
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
@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(). Уверен, существует множество интересных способов, в которых данная функция пригодилась бы.
Автор: Emil Björklund
Источник: //css-tricks.com/
Редакция: Команда webformyself.
* Признана экстремистской организацией и запрещена в Российской Федерации.