Minulý týden jsem dělal optimalizaci obrázků na webu htmlfactory.cz. A protože se mi to povedlo nad očekávání rozhodl jsem se o tom napsat článek a podělit se o tom s vámi. Článek je pro webové vývojáře, kteří tvoří wordpress šablony na míru. Pro wordpress uživatele s kupovanými šablony tento článek není vhodný.
Než jsem se do samotné optimalizace vůbec pustil stanovil jsem si cíle, kterých jsem chtěl dosáhnout. V mém případě to byly:
- zkomprimovat obrázky do co nejmenší datové velikosti a zároveň neztratit kvalitu
- všechny obrázky (jpg,png) musí být i ve .webp formátu
- obrázky načítat lazyloadingem
- rozmazaný – velmi datově malý obrázek použit jako placeholder
- velkým bonusem je umístění obrázků na CDN
- pokud možno snadnou implementaci
- co nejlevnější – nebráním se placeným pluginům, ale 300kč měsíčně za obrázky na webu se mi platit nechce
- zlepšit rychlost načtení webu díky optimalizaci obrázků
Dalším mým přáním bylo získané zkušenosti a postupy následně používat i na projektech pro mé klienty. Naučit se jednou a pak používat všude.
A protože požadavků jsem neměl málo, několik dnů až týdnů jsem nad tím přemýšlel. Hledal jsem pluginy a četl jejich dokumentaci. Našel jsem mnoho pluginů, které nabízejí optimalizaci obrázků ve wordpressu. Většinou byly placené a ne zrovna levné. Přišlo mi nesmyslné platit 300 kč i více měsíčně, za optimalizaci obrázků na vlastním webu. I kdybych do takového řešení šel pro svůj vlastní web, tak velmi pochybuji, že bych něco takového prosadil pro mé klienty, pro které tvořím weby na wordpressu.
Pluginy byly nejen drahé, ale zároveň nesplňovaly mé požadavky. Chyběl .webp formát, nebo CDN, nebo plugin byl developer unfriendly. Prostě vždy něco chybělo..
A tak jsem to ošťouchaval ze všech stran a pořád hledal dál. Už jsem měl pocit, že není možné obrázky wordrpessu rozumně optimalizovat dle moderních trendů a technologii..
Pak jsem narazil na článek mistrovská optimalizace obrázků nejen pro WordPress. Zahlédl jsem v něm .webp formát, cdn a poznámku, že návod je spíše pro vývojáře. Velmi mě to zaujalo a po přečtení jsem měl pocit, že to je ONO – řešení, které hledám.
Optimalizaci obrázků jsem prováděl dle postupu v článku, proto se nebudu rozepisovat co jsem dělal. Místo toho vám ukážu čeho jsem dosáhl a jak to zhruba funguje.
Jak to funguje
Řešení je vázané na službu cloudinary.com, která je sice placená, ale nabízí velmi velkorysou verzi zdarma. Po implementaci tohoto řešení, dosahuji cca 5% z verze zdarma. To znamená, že mám ještě dalších 95% k využití. Proto bych se nebál toto řešení použit na projekty menších až středních velikostí.
Velkou výhodou tohoto řešení je, že optimalizace obrázků neprobíhá ve wordpressu, ale ve službě cloudinary. Nemusíme tak dlouze čekat při nahrávání nových médii.
Služba cloudinary vyřešila tyto moje požadavky:
- velmi dobrá komprese
- .webp formát
- rozmazaný placeholder
- CDN
- poměrně snadná implementace
Jednou z velmi šikovných funkci je automatická detekce a vložení .webp formátu. Nemusíte dělat vůbec nic, cloudinary sám rozpozná jestli váš prohlížeč podporuje .webp formát a pokud ano, automatický vám obrázek pošle v tomto formátu. Žádné picture tagy, žádné další starosti s převodem, vše plně automatické 🙂
Jinými slovy služba cloudinary vyřešila téměř vše co jsem potřeboval. Chyběl mi už jen lazyloading. Naštěstí plugin generuje pouze URL adresu, proto vývojář má velký prostor pro přizpůsobení html dle vlastních potřeb. Tak hůra do kódu.
Technikálie
Nejdříve jsem vytvořil 13 velikostí.
<?php
$sizes = array(10, 50, 150, 300, 500, 600, 700, 1000, 1280, 1366, 1600, 1920, 2560);
?>
Nejmenší 10px používám jako placeholder, než se načte potřebná velikost obrázku. A protože chci, aby obrázky na webu byly dohledatelné i ve vyhledávačích používám noscript.
<?php
$img = '<img
src="'.$placeholder_src.'"
data-srcset="'.$srcset.'"
data-src="'.$fallback_src.'"
data-sizes="auto"
'.$attributes.'
>';
$noscript_img = '<noscript>
<img
src="'.$fallback_src.'"
srcset="'.$srcset.'"
sizes="(max-width: '.$width.'px) 100vw, '.$width.'px"
'.$attributes.'
>
</noscript>';
?>
Všimněte si řádku číslo 6, kde je data-sizes=“auto“. Je to tam proto, že používám knihovnu lazysizes, která se stára o sizes atribut automatický. Před načtením obrázku a při změně viewportu, lazysizes javascriptem nastaví aktuální velikost obrázku v pixelech. Tuto funkci miluji, protože nemusíte sizes atribut vytvářet sami, čímž si ušetříte fakt hodně času. Automatickou detekci a změnou sizes atributu si prohlížeč následně sám stáhne pro něj nejvhodnější obrázek z těch 13, které jsem si vytvořil.
.no-js img.lazyload {
display: none;
}
.blur-up {
filter: blur(5px);
transition: filter 400ms;
}
.blur-up.lazyloaded, .no-js .blur-up{
filter: blur(0);
}
.no-js noscript .lazyload{
display: block;
}
Zbývá už jen trochu css kódu, který přidá blur efekt při načtení obrázku. Pozor na web s vypnutým javascriptem – je nutné schovat lazyloadingový obrázek a zobrazit noscript obrázek.
Pokud by někoho zajímal celý php kód, tak je tady:
<?php
//wp_get_attachment_image_src alternative with cloudinary
function my_theme_image_src( $id, $width, $cloudinaryAtrs = [] ) {
if(!$cloudinaryAtrs['crop']) $cloudinaryAtrs['crop'] = 'fill';
if(!$cloudinaryAtrs['quality']) $cloudinaryAtrs['quality'] = 'auto:good';
if(!$cloudinaryAtrs['fetch_format']) $cloudinaryAtrs['fetch_format'] = 'auto';
if ( function_exists( 'cloudinary_url' ) ) {
// if "Auto Cloudinary" plugin exists -> get the image url with the specified and predefined parameters from Cloudinary service
$args = array(
'transform' => array(
'width' => $width,
'crop' => $cloudinaryAtrs['crop'],
'quality' => $cloudinaryAtrs['quality'],
'fetch_format' => $cloudinaryAtrs['fetch_format'],
),
);
$image_url = cloudinary_url( $id, $args );
} elseif ( function_exists( 'fly_add_image_size' ) ) {
// if "Auto Cloudinary" plugin doesn't exist but "Fly Dynamic Image Resizer" exists -> get the image with the specified width and height from local server
$img = fly_get_attachment_image_src( $id, array( $width, $height ), true );
$image_url = $img['src'];
} else {
// if neither plugin works -> get only the url of the image from local server
$image_url = wp_get_attachment_image_src( $id )[0];
}
return esc_url( $image_url );
}
//wp_get_attachment_image_srcset alternative with cloudinary
function my_theme_image_srcset( $id, $cloudinaryAtrs = [] ) {
$sizes = array(10, 50, 150, 300, 500, 600, 700, 1000, 1280, 1366, 1600, 1920, 2560);
$srcset = [];
if ( function_exists( 'cloudinary_url' ) ) {
// if "Auto Cloudinary" plugin exists -> get the image url with the specified and predefined parameters from Cloudinary service
foreach ($sizes as $size) {
array_push($srcset, str_replace(',', '%2C', my_theme_image_src( $id, $size, $cloudinaryAtrs ))." ".$size."w");
}
$srcset = implode(', ', $srcset);
} elseif ( function_exists( 'fly_add_image_size' ) ) {
// if "Auto Cloudinary" plugin doesn't exist but "Fly Dynamic Image Resizer" exists -> get the image with the specified width and height from local server
foreach ($sizes as $size) {
array_push($srcset, fly_get_attachment_image_src( $id, $size, true )['src']." ".$size."w");
}
$srcset = implode(', ', $srcset);
} else {
// if neither plugin works -> get only the url of the image from local server
$srcset = wp_get_attachment_image_srcset( $id );
}
return esc_attr( $srcset );
}
//custom image full html
function my_theme_image($id, $width = 500, $attrs = [], $cloudinaryAtrs = [] ){
if(!$attrs['alt']) $attrs['alt'] = esc_attr( get_post_meta( $id, '_wp_attachment_image_alt', true ) );
if(!$cloudinaryAtrs['crop']) $cloudinaryAtrs['crop'] = 'fill';
if(!$cloudinaryAtrs['quality']) $cloudinaryAtrs['quality'] = 'auto:eco';
$placeholder_src = my_theme_image_src( $id, 10, $cloudinaryAtrs );
$fallback_src = my_theme_image_src( $id, $width, $cloudinaryAtrs );
$srcset = my_theme_image_srcset($id, $cloudinaryAtrs);
$image_fullsize_src = wp_get_attachment_image_src( $id, 'full' );
$attrs['class'] = $attrs['class'].' lazyload blur-up';
$attrs['width'] = $image_fullsize_src[1];
$attributes = '';
foreach ($attrs as $key => $value) {
$attributes .= $key.'="'.$value.'" ';
}
$filetype = wp_check_filetype($image_fullsize_src[0])['ext'];
if('svg' == $filetype){
$cloudinaryAtrs['fetch_format'] = $filetype;
$img = '<img
src="'.$placeholder_src.'"
data-src="'.my_theme_image_src( $id, $width, $cloudinaryAtrs ).'"
'.$attributes.'
>';
$noscript_img = '<noscript>
<img
src="'.my_theme_image_src( $id, $width, $cloudinaryAtrs ).'"
'.$attributes.'
>
</noscript>';
}else{
$img = '<img
src="'.$placeholder_src.'"
data-srcset="'.$srcset.'"
data-src="'.$fallback_src.'"
data-sizes="auto"
'.$attributes.'
>';
$noscript_img = '<noscript>
<img
src="'.$fallback_src.'"
srcset="'.$srcset.'"
sizes="(max-width: '.$width.'px) 100vw, '.$width.'px"
'.$attributes.'
>
</noscript>';
}
return $img.$noscript_img;
}
//způsob použití
$thumbnail_id = get_post_thumbnail_id();
echo my_theme_image(
$thumbnail_id, //do funkce vkládáme vždy ID obrázku
500, //jedna z 13 velikosti a zároveň největší velikost, které obrázek dosáhne (obvyklle obrázek v této velikosti načte v IE, který nepodporuje srcset a sizes atributy)
['class' => 'embed-responsive-item'] //do array lze uvést další atributy jako je class, alt, apod
);
Funkci se dá použit pro generaci srcset a src atributu například pro fancybox.
<button
type="button"
data-fancybox="fancybox-id"
data-src="<?php echo esc_attr(my_theme_image_src($image_id, 1280 )) ?>"
data-srcset="<?php echo my_theme_image_srcset($image_id) ?>"
>
Shrnutí
Díky tomuto řešení se mi podařilo splnit všechny mé cíle a nestálo mě to ani korunu. Optimalizovaný web nyní vykazuje perfektní čísla v webpagetest, lighthouse a pagespeed insights. Způsob načtení obrázku – nejdříve rozmazaná nekvalitní verze a líné načtení správné verze, patří dnes do nejlepších způsobů pro načtení obrázků, které znám. Tento způsob využívá například medium nebo gatsbyjs a jemu podobné jamstack frameworky.