Blur Up плейсхолдер для next/image
Неделю назад `Vercel` анонсировали 10ую версию своего фреймворка next.js
, куда в том числе вошёл новый компонент для отрисовки изображений - next/image
. В этой статье я не буду говорить о его плюсах/минусах, а лишь опишу свой опыт доработки этого компонента под свои нужды. Мне хотелось, чтобы пользователь во время загрузки изображений мог наблюдать эффект blur up - когда маленькая копия оригинального изображения растягивается на оригинальную длину\ширину и блурится, будто говоря: "я вот-вот уже почти загрузилась".
Еще до того, как появился компонент от самих next.js
, я пользовался собственной поделкой, которая умела в lazy loading даже для изображений без конкретных размеров (в моём блоге превалируют именно такие изображения), а так же поддавалась кроулингу.
const LazyImage = ({ alt, ratio, src, ...props }) => {
const ref = useRef(null);
const [size, setSize] = useState({});
const [imgSrc, setImgSrc] = useState(undefined);
const [loaded, setLoaded] = useState(false);
const handleLoad = useCallback(() => {
setLoaded(true);
ref.current.removeEventListener('load', handleLoad);
}, []);
// 1. Set image width and height after mount, when width is available
useEffect(() => {
const { width } = ref.current.getBoundingClientRect();
const height = width / ratio;
setSize({
height,
width,
});
}, []);
// 2. When dimensions are known set src to start loading image
useEffect(() => {
if (size.width && size.height) {
setImgSrc(src);
}
}, [size, src]);
// 3. Add onload callback to reset width and height to make pictures responsive again
useEffect(() => {
if (imgSrc) {
ref.current.addEventListener('load', handleLoad);
}
}, [handleLoad, imgSrc]);
return (
<>
<img
alt={alt}
height={loaded ? undefined : size.height}
loading="lazy"
ref={ref}
src={imgSrc}
width={loaded ? undefined : size.width}
{...props}
/>
<noscript>
<img
alt={alt}
loading="lazy"
src={src}
{...props}
/>
</noscript>
</>
);
};
Уже тогда я хотел что-то вроде блура для своих изображений, но как-то лениво было этим заняться. И вот час настал. Касательно теории, next/image
повторяет дизайн обычного img
, но требует обязательно наличие ширины\длины. При чём, как утверждает документация, их img
по умолчанию отзывчивый:
width and height are required to prevent Cumulative Layout Shift, a Core Web Vital that Google is going to use in their search ranking
width and height are automatically responsive, unlike the HTML <img> element;
Это нас устраивает. Но стоит учитывать, что при рендере этого компонента сам img
окажется обёрнут ещё в два div
-а. И лично меня это немного озадачило, т.к. все мои изображения рисовались внутри инлайнового <p>
.
От слов к делу. Давайте я сразу покажу код готового компонента, а потом разберёмся, что он делает:
const LazyImageWithPlaceholder = ({ alt, className, height, placeholderSrc, src, width, ...props }) => {
const [loaded, setLoaded] = useState(false);
const style = {
background: loaded ? 'none' : `url(${placeholderSrc}) no-repeat center center`,
backgroundSize: loaded ? 'initial' : 'cover',
filter: loaded ? 'none' : 'blur(20px)',
transition: 'filter .3s ease-out',
};
const handleLoad = useCallback(() => {
setLoaded(true);
}, []);
return (
<div className={className} style={style}>
<Image
alt={alt}
height={height}
onLoad={handleLoad}
src={src}
width={width}
{...props}
/>
</div>
);
};
export default LazyImageWithPlaceholder;
Начнём, пожалуй, с названия. Оно немного длинное лишь потому, что этот компонент на самом деле рисуется внутри другого - LazyImage
. Например, когда мне не нужен плейсхолдер, то логика присущая только плейсхолдеру не будет выполнена:
const LazyImage = ({ ...props }) => {
if (placeholderColor) {
return (
<LazyImageWithColor
// ...
/>
);
}
if (placeholderSrc) {
return (
<LazyImageWithPlaceholder
// ...
/>
);
}
return (
<Image
// ...
/>
);
};
В коде выше есть небольшой спойлер - LazyImageWithColor
. Этот компонент позволяет вместо blur up выдавать доминантный цвет изображения, пока оно грузится.
Но вернёмся к LazyImageWithPlaceholder
. В нём есть два момента, на которые стоит обратить внимание.
placeholderSrc
получаем из пропсов- добавляется третий
div
поверх двух отnext/image
Чтобы вывести заблюреное изображение, его нужно как-то произвести. Делать это будем с помощью lqip-loader
. Этот лоадер для вебпака позволяет импортировать изображения в уже заблюреном виде. Использовать его можно напрямую (донастроив вебпак руками) или воспользоваться готовыми "решениями": next-lqip-images
/next-optimized-images
. Давайте рассмотрим второй вариант с next-lqip-images
. Вся настройка описана в репозитории, я не буду на ней останавливаться. Смотрим, как этим пользоваться:
import catPhoto from './cat.png?lqip';
<LazyImage
alt="Котейка"
height={500}
placeholderSrc={catPhoto.dataURI}
src={catPhoto.src}
width={327}
/>
После импорта catPhoto
это объект, в котором src
- ссылка на оригинальное изображение, а dataURI
- заблюреное изображение в кодировке base64. Если мне не нужен плейсхолдер, то просто не передаю его в пропсах. А если вообще хочу отключить любую оптимизацию и видеть оригинал, то передаю в пропсы unoptimized
:
import catPhoto from './cat.png';
// lazy-loading без blur up
<LazyImage
alt="Котейка"
height={500}
src={catPhoto}
width={327}
/>
import catPhoto from './cat.png';
// lazy-loading без оптимизации
// (по умолчанию картинки выводятся с качеством 75%)
<LazyImage
alt="Котейка"
height={500}
src={catPhoto}
unoptimized
width={327}
/>
На этом как бы и всё, компонент очень простой. Из доработок можно подумать над тем, чтобы блюрить изображение не сразу, а спустя какое-то небольшое время. Чтобы для уже закешированных изображений не показывать blur up. Работу этого компонента можете проверить почти на любой картинке моего сайта.
У меня нет особо времени, чтобы оформить этот компонент в виде библиотеки, поэтому, если кто-то желает этим заняться - вперёд! Оставьте только ссылку на эту страницу, как на источник вдохновения и я вам буду очень благодарен.