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

Никита Дубко, iTechArt Group

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


Никита Дубко, iTechArt Group

Никита Дубко

MinskCSS MinskJS CSS-Minsk-JS Никита Дубко
3 секунды — оптимально

53% вероятность, что пользователь покинет страницу, если мобильный сайт загружается дольше, чем
3 секунды

Http Archive

Lighthouse

PWA Lighthouse
Chrome Audits
Useful tooltip
trollface Frontend Conf Audit Results

Путь изображения

Сохранение
на сервере

HDD

Форматы изображений

Растровая графика

Растр

JPEG

Joint Photographic Experts Group
jpeg.org

JPEG logo
R G B
RGB
YUV
Y
яркость
V (Cb) хроматический синий
U (Cr) хроматический красный

Конвейер JPEG

  1. Преобразование RGB в YUV

Размер: 225x300

100
q=100; 33 КБ
70
q=70; 11 КБ
30
q=30; 4 КБ
0
q=0; 3 КБ

Размер: 225x300

100
q=100; 71 КБ
70
q=70; 24 КБ
30
q=30; 9 КБ
0
q=0; 5 КБ

Baseline JPEG

Baseline JPEG

Progressive JPEG

Progressive JPEG

Особенности progressive JPEG

Используют прогрессивный JPEG

Субдискретизация (chroma subsampling)

4:4:4 (1x1) Без субдискретизации

Субдискретизация (chroma subsampling)

4:4:4 (1x1) Без субдискретизации
4:2:0 (2x2) Горизонтальная и вертикальная
Subsampling

Размер: 225x300

1x1
q=70; 1x1; 13 КБ
q=70; 2x1; 11 КБ
q=70; 2x2; 9 КБ

Инструменты

Измерение искажений

Guetzli

Gulp

npm install --save-dev gulp-imagemin

const gulp = require('gulp');
const imagemin = require('gulp-imagemin');


gulp.task('images', () => 
    gulp.src('images/*.jpg')
        .pipe(imagemin([
            imagemin.jpegtran({ progressive: true }),
        ]))
        .pipe(gulp.dest('dist'))
);

npm install --save-dev imagemin-guetzli

const gulp = require('gulp');
const imagemin = require('gulp-imagemin');
const imageminGuetzli = require('imagemin-guetzli');


gulp.task('guetzli', () => 
    gulp.src('src/*.jpg')
        .pipe(imagemin([ imageminGuetzli({ quality: 85 }) ]))
        .pipe(gulp.dest('dist'))
);

npm install --save-dev imagemin-mozjpeg

const gulp = require('gulp');
const imagemin = require('gulp-imagemin');
const imageminMozjpeg = require('imagemin-mozjpeg');
gulp.task('mozjpeg', () =>
    gulp.src('src/*.jpg')
        .pipe(imagemin([
            imageminMozjpeg({
                progressive: true,
                quality: 85,
                sample: ['2x1']
            })
        ]))
        .pipe(gulp.dest('dist'))
);

Как выбрать?

Guetzli MozJPEG
  • для статических сайтов;
  • важно качество;
  • не жалко оперативной памяти.
  • для динамических сайтов;
  • важна скорость;
  • контроль над настройками.

Лайфхак 1: Размытие

Размер: 1200x925

100

q=100; 759 КБ

80

q=80; 383 КБ

80 and blur

q=80 (blur); 206 КБ

Лайфхак 2: Комбинация сжатий

gulp.task('jpegcombo', () =>
    gulp.src('src/*.jpg')
        .pipe(imagemin([ imageminGuetzli({ quality: 95 }) ]))
        .pipe(imagemin([
            imageminMozjpeg({
                progressive: true,
                quality: 85,
            })
        ]))
        .pipe(gulp.dest('dist'))
);
source
Source (1350x900); 1126 КБ
guetzli 95
Guetzli q=95; 434 КБ
mozjpeg 85
+ MozJPEG q=85; 209 КБ
mozjpeg 85 2x2
+ MozJPEG q=85 2x2; 188 КБ

Лайфхак 3: EXIF

const shell = require('gulp-shell');

gulp.task('exif', () =>
    gulp.src('dist/*.jpg', { read: false })
        .pipe(shell([
            'exiftool -P -overwrite_original -all= <%=file.path%>'
        ]))
);
JPEG2000 JPEG-XR

brew install ImageMagick
...

convert image.jpg -quality 0 image.jp2

convert image.jpg image.jxr

*.jp2 — JPEG2000
*.jxr — JPEG-XR

GIF

Graphics Interchange Format

Cat

Особенности GIF

GIF

GIF -> MP4

ffmpeg \
-i animated.gif \
-movflags faststart \
-pix_fmt yuv420p \
-vf “scale=trunc(iw/2)*2:trunc(ih/2)*2” \
video.mp4

ffmpeg.org

GIF -> WebM

ffmpeg -i input.gif -c vp9 -b:v 0 -crf 41 output.webm

<img src="kitty.gif" width="400" height="300">

⬇️

<video width="400" height="300"
       autoplay loop muted playsinline>
    <source src="kitty.webm" type="video/webm">
    <source src="kitty.mp4" type="video/mp4">
    <img src="kitty.gif" width="400" height="300">
</video>

Gifsicle

gifsicle -O3 --lossy=80 -o output.gif input.gif

kohler/gifsicle — lossless
kornelski/giflossy — lossy

npm install --save-dev imagemin-gifsicle

const gulp = require('gulp');
        const imagemin = require('gulp-imagemin');
        const imageminGifsicle = require('imagemin-gifsicle');
        

        gulp.task('gifsicle', () => 
            gulp.src('src/*.gif')
                .pipe(imagemin([ imageminGifsicle() ]))
                .pipe(gulp.dest('dist'))
        );

PNG

Portable Network Graphics
«PNG is Not GIF»

libpng.org PNG Transparency Demonstration

Особенности PNG

PNG GIF
PNG GIF
TinyPNG

tinypng.com

PNGA example

Инструменты

npm install --save-dev imagemin-pngquant

const gulp = require('gulp');
const imagemin = require('gulp-imagemin');
const imageminPngquant = require('imagemin-pngquant');


gulp.task('pngquant', () => 
    gulp.src('src/*.png')
        .pipe(imagemin([ imageminPngquant({ quality: 85 }) ]))
        .pipe(gulp.dest('dist'))
);

WebP

WebP

developers.google.com/speed/webp/

Особенности WebP

Используют WebP

npm install --save-dev imagemin-webp

const imageminWebp = require('imagemin-webp');


gulp.task('webp', () => 
    gulp.src('src/*.{jpg,png}')
        .pipe(imagemin([
             imageminWebp({
                 quality: 85,
                 preset: 'photo'
             })]))
        .pipe(gulp.dest('dist'))
);
JPG Работает только в Chrome, извините
JPG; q=100; 71 КБ WebP; q=100; 36 КБ
JPG Работает только в Chrome, извините
JPG; q=70; 24 КБ WebP; q=70; 7 КБ

gif2webp -mixed kitty.gif -o kitty.webp


GIF Работает только в Chrome, извините
GIF; 1.3 MБ WebP; 582 КБ

Векторная графика

SVG

Scalable Vector Graphics
w3.org/Graphics/SVG

SVG

Особенности SVG

<svg id="circle" height="200" xmlns="http://www.w3.org/2000/svg">
    <circle id="greencircle" cx="50" cy="50" r="50" fill="green" />
</svg>
PNG SVG
PNG; 37 КБ SVG; 5 КБ

Инструменты

SVGOMG

npm install --save-dev imagemin-svgo

const imageminSvgo = require('imagemin-svgo');


gulp.task('svgo', () => 
    gulp.src('src/*.svg')
        .pipe(imagemin([
             imageminSvgo({
                 convertColors: true,
             })]))
        .pipe(gulp.dest('dist'))
);
Source Result
Source; 27 КБ Minified; 12 КБ

sprite.svg

<svg xmlns="http://www.w3.org/2000/svg"
     xmlns:xlink="http://www.w3.org/1999/xlink">
    <symbol viewBox="0 0 100 100" id="greencircle">
        <circle cx="50" cy="50" r="50" fill="green" />
    </symbol>
    <symbol viewBox="0 0 100 100" id="redsquare">
        <rect x="0" y="0" width="100" height="100" fill="red" />
    </symbol>
</svg>

index.html

<html>
<body>
    <svg class="icon icon--circle" width="100">
        <use xlink:href="sprite.svg#greencircle" />
    </svg>
    <svg class="icon icon--square" width="100">
        <use xlink:href="sprite.svg#redsquare" />
    </svg>
</body>
</html>

jonathantneal/svg4everybody

npm install --save-dev gulp-svg-sprite

const svgSprite = require('gulp-svg-sprite');
gulp.task('svgSprite', () =>
    gulp.src('src/icons/*.svg')
        .pipe(svgSprite({
            mode: {
                symbol: { sprite: "../sprite.svg" }
            }
        }))
        .pipe(gulp.dest('dist'))
);
        

Советы

Отправка клиенту
по запросу

<table> + background-imagex3

cyanharlow/purecss-francine

Текст на картинке

JPG; 25 КБ

WTF

Server-side content-type negotiation

GET /some.jpg HTTP/1.1
Host: ...
Connection: keep-alive
User-Agent: ...
Accept: image/webp,image/apng,image/*,*/*;q=0.8
Accept-Encoding: gzip, deflate, br
Accept-Language: en-US,en;q=0.9,ru-RU;q=0.8,ru;q=0.7
<IfModule mod_rewrite.c>
  RewriteEngine On
  RewriteCond %{HTTP_ACCEPT} image/webp
  RewriteCond %{DOCUMENT_ROOT}/$1.webp -f
  RewriteRule (.+)\.(jpe?g|png)$ $1.webp [T=image/webp,E=accept:1]
</IfModule>

<IfModule mod_headers.c>
  Header append Vary Accept env=REDIRECT_accept
</IfModule>

AddType image/webp .webp

HTTP Cache

localStorage

Service Workers

if ('serviceWorker' in navigator) {
    const sw = navigator.serviceWorker;
    sw.register('sw.js')
        .then(() => sw.ready.then((worker) => {
            worker.sync.register({
                minRequiredNetwork: 'network-online'
            });
        }));
}
importScripts('https://storage.googleapis.com/.../workbox-sw.js');
workbox.routing.registerRoute(
    /\.(?:png|gif|jpg|jpeg|svg)$/,
    workbox.strategies.cacheFirst({
        cacheName: 'images',
        plugins: [
            new workbox.expiration.Plugin({
                maxEntries: 60,
                maxAgeSeconds: 30 * 24 * 60 * 60, // 30 Days
            }),
        ],
    }),
);
pwabuilder.com

Network Type

let isSlow = true;
const connection = navigator.connection;
if (connection) {
    // 'slow-2g', '2g', '3g' или '4g'
    if (connection.effectiveType !== 'slow-2g') {
        isSlow = false;
    }
}

Декодирование и отображение в браузере

Dev Tools
1800x1200

Отображение

  1. декодирование;
  2. изменение размеров;
  3. отрисовка.

Не заставляйте браузер обрабатывать лишние пиксели!

2008

<img src="rit.jpg" alt="Привет, РИТ!" width="20" height="18" />

2018

<picture>
    <source type="image/webp" media="(min-width: 1150px)"
            srcset="rit-desktop@1x.webp 1x, rit-desktop@2x.webp 2x">
    <source type="image/webp"
            srcset="rit-mobile@1x.webp 1x, rit-mobile@2x.webp 2x">
    <source media="(min-width: 1150px)"
            srcset="rit-desktop@1x.jpg 1x, rit-desktop@2x.jpg 2x">
    <img src="rit-mobile@1x.jpg" srcset="rit-mobile@2x.jpg 2x"
            alt="Привет, РИТ!" width="20" height="18">
</picture>
<picture>
    <source type="image/webp" media="(min-width: 1150px)"
            srcset="rit-desktop@1x.webp 1x, rit-desktop@2x.webp 2x">
    <source type="image/webp"
            srcset="rit-mobile@1x.webp 1x, rit-mobile@2x.webp 2x">
    <source media="(min-width: 1150px)"
            srcset="rit-desktop@1x.jpg 1x, rit-desktop@2x.jpg 2x">
    <img src="rit-mobile@1x.jpg" srcset="rit-mobile@2x.jpg 2x"
            alt="Привет, РИТ!" width="20" height="18">
</picture>
<img srcset="rit-320w.jpg 320w,
             rit-480w.jpg 480w,
             rit-800w.jpg 800w"
     sizes="(max-width: 320px) 280px,
            (max-width: 480px) 440px,
            800px"
     src="rit-800w.jpg" alt="Привет, РИТ!">

scottjehl/picturefill

2008

.rit {
    background-image: url('img/rit.jpg');
}

2018

.rit {
    background-image: url('img/rit-mobile.jpg');
}

@media (min-width: 1050px) {
    .rit {
        background-image: url('img/rit-desktop.jpg');
    }
}
iPhone 4
2dppx
iPhone X
3dppx
Samsung Galaxy S8
4dppx
.rit {
    background-image: url('img/rit@1x.jpg');
}

@media (-webkit-min-device-pixel-ratio: 2),
       (min-resolution: 192dpi),
       (min-resolution: 2dppx) {
    .rit {
        background-image: url('img/rit@2x.jpg');
    }
}
.rit {
    background-image: image-set(url('rit@1x.jpg') 1x,
                                url('rit@2x.jpg') 2x);
}

filamentgroup/imaging-heap

npm install --global imaging-heap

imagingheap https://ritfest.ru/moscow/2018


<img style="top:0" src="/i/icons/s_backendconf.png" alt="Backend Conf">
╔══════════╤══════════╤═══════╤════════════╤═══════╤════════════╗
║          │ Image    │ @1x   │ @1x        │ @2x   │ @2x        ║
║          │ Width in │ Image │ Percentage │ Image │ Percentage ║
║ Viewport │ Layout   │ Width │ Match      │ Width │ Match      ║
╟──────────┼──────────┼───────┼────────────┼───────┼────────────╢
║ 320px    │ 16px     │ 16px  │ 100.0%     │ 16px  │ 50.0%      ║
║ 480px    │ 16px     │ 16px  │ 100.0%     │ 16px  │ 50.0%      ║
║ 640px    │ 16px     │ 16px  │ 100.0%     │ 16px  │ 50.0%      ║
║ 800px    │ 16px     │ 16px  │ 100.0%     │ 16px  │ 50.0%      ║
║ 960px    │ 16px     │ 16px  │ 100.0%     │ 16px  │ 50.0%      ║
║ 1120px   │ 16px     │ 16px  │ 100.0%     │ 16px  │ 50.0%      ║
║ 1280px   │ 16px     │ 16px  │ 100.0%     │ 16px  │ 50.0%      ║
╚══════════╧══════════╧═══════╧════════════╧═══════╧════════════╝
Legend:  @1x <100% >150%    Above @1x <75% 75%–92%

Lazy Loading

Тыжпрограммист!
<img src="placeholder.jpg"
     data-src="image.jpg" />
const observer = new IntersectionObserver(handler, {
    rootMargin: '0px',
    threshold: 0.1,
});

const images = document.querySelectorAll('img[data-src]');
images.forEach(img => {
    observer.observe(img);
});
function handler(entries, observer) {
    entries.forEach(entry => {
        if (entry.isIntersecting) {
            loadImage(entry.target);
            observer.unobserve(entry.target);
        }
    });
}
function loadImage(image) {
    const src = image.dataset.src;
    fetchImage(src).then(() => { image.src = src; });
}
function fetchImage(url) {
    return new Promise((resolve, reject) => {
        const image = new Image();
        image.src = url;
        image.onload = resolve;
        image.onerror = reject;
    });
}

Placeholder — техники

Дано

69 КБ
31 КБ
50 КБ

Общий placeholder

Фоновый цвет

Low Quality Image Placeholders (LQIP)

2.9 КБ
2.8 КБ
2.9 КБ

LQIP + размытие

SVG-Based Image Placeholder (SQIP)

730 Б
972 Б
959 Б

npm install -g sqip

sqip -o sqip.svg -n 8 image.jpg

Primitive (ellipses)

38 КБ
38 КБ
38 КБ

Primitive (triangles)

23 КБ
23 КБ
23 КБ

@geometrizer

Предзагрузка

<link rel="preload" as="image" href="important.png">

Доступность

weblind.ru

aardrian/pen/wjWyvg

Защита от роботов #1

robots.txt

User-agent: Googlebot-Image
Disallow: /images/secret.jpg 

Защита от роботов #2

HTTP Headers

<IfModule mod_headers.c>
  <FilesMatch "images/*\.jpg$">
    Header append X-Robots-Tag "noindex"
  </FilesMatch>
</IfModule>

Приемчики 🚀

PNG -> JPEG + SVG-маска

PNG; 203 КБ

JPEG + SVG; 75 КБ


<svg width="300" height="300" viewBox="0 0 300 300">
    <defs>
        <clipPath id="octocat">
            <path fill="#000000" d="..."/>
        </clipPath>
    </defs>
    <image clip-path="url(#octocat)"
           width="300" height="300"
           xlink:href="dojocat.jpg" />
</svg>
        
Контрастная фотография

399 КБ

Фото без контраста

349 КБ

.uncontrasted {
    filter: contrast(1.15);
}

Compressive Images

q=80; w=350px; 19 КБ

q=0; w=700px; 8 КБ

Советы

Материалы

Видео

Спасибо за внимание!

mefody.github.io/talks/images-delivery/
@dark_mefody
n.a.dubko@gmail.com

QR code — ссылка на презентацию