Улучшаем восприятие производительности сайта: изменение размера изображения по требованию

Данная статья – это часть серии публикаций о создания примера блога с фотогалереей. (Смотрите репозиторий здесь).

Мы создали веб-приложение (блог с фотогалереей) для анализа производительности и оптимизации. На данный момент приложение выводит одинаковое изображение независимо от разрешения и размера экрана устройства. В данном руководстве мы реализуем загрузку версии изображения в зависимости от размера экрана пользовательского устройства.

Содержание

Цель

Реализация пройдет в два этапа:

  1. Сделать изображения адаптивными – миниатюры на домашней странице, страницах в галерее. Полноразмерная версия будет открываться при нажатии на изображение в галерее.
  2. Добавление функционала для изменения размера изображения на лету.

Адаптивные изображения?

Теперь вместо простого тега <img src=»mypic.jpg»> используются более сложные конструкции:

<picture>
<source media="(max-width: 700px)" sizes="(max-width: 500px) 50vw, 10vw"
srcset="stick-figure-narrow.png 138w, stick-figure-hd-narrow.png 138w">

<source media="(max-width: 1400px)" sizes="(max-width: 1000px) 100vw, 50vw"
srcset="stick-figure.png 416w, stick-figure-hd.png 416w">

<img src="stick-original.png" alt="Human">
</picture>

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

Добавляем srcset

Первоначальная обработка изображений осуществляется в файле home-galleries-lazy-load.html.twig. Он отвечает за вывод списка галерей на главной странице.

<a class="gallery__link" href="{{ url('gallery.single-gallery', {id: gallery.id}) }}">
  <img src="{{ gallery.images.first|getImageUrl }}" alt="{{ gallery.name }}"
    class="gallery__leading-image card-img-top">
</a>

Ссылка на изображение извлекается из фильтра Twig, который расположен в файле src/Twig/ImageRendererExtension.php. Он берет идентификатор изображения, имя маршрута (определен в аннотации в маршруте ImageController serveImageAction) и генерирует URL-адрес на основе следующей формулы: /image/{id}/raw -> заменяя {id} на указанный идентификатор:

public function getImageUrl(Image $image)
{
  return $this->router->generate('image.serve', [
      'id' => $image->getId(),
  ], RouterInterface::ABSOLUTE_URL);
}

Заменим код, приведенный выше, на следующий:

public function getImageUrl(Image $image, $size = null)
{
  return $this->router->generate('image.serve', [
      'id' => $image->getId() . (($size) ? '--' . $size : ''),
  ], RouterInterface::ABSOLUTE_URL);
}

Теперь все URL-адреса изображений будут иметь суффикс —x, где x – это их размер. Это изменение мы применим и к тегу img:

<a class="gallery__link" href="{{ url('gallery.single-gallery', {id: gallery.id}) }}">
  <img src="{{ gallery.images.first|getImageUrl }}"
       alt="{{ gallery.name }}"
       srcset="
           {{ gallery.images.first|getImageUrl('1120') }}  1120w,
           {{ gallery.images.first|getImageUrl('720') }} 720w,
           {{ gallery.images.first|getImageUrl('400') }}  400w"
       class="gallery__leading-image card-img-top">
</a>

После обновления домашней страницы, в атрибуте srcset будут указаны новые размеры:

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

<a class="gallery__link" href="{{ url('gallery.single-gallery', {id: gallery.id}) }}">
  <img src="{{ gallery.images.first|getImageUrl('250') }}"
       alt="{{ gallery.name }}"
       class="gallery__leading-image card-img-top">
</a>

Теперь миниатюры появляются по требованию. Но они кэшируются и извлекаются после того, как уже сгенерированы.

В templates/gallery/single-gallery.html.twig применяем то же исправление. Мы имеем дело с миниатюрами, поэтому просто сожмем файл, добавив размер в фильтр getImageUrl:

<img src="{{ image|getImageUrl(250) }}" alt="{{ image.originalFilename }}"
    class="single-gallery__item-image card-img-top">

Переходим к реализации srcset. Отдельные окна просмотра изображения выводятся с помощью модального окна JavaScript в нижней части того же окна просмотра галереи:

parent() }}

    <script>
        $(function () {
            $('.single-gallery__item-image').on('click', function () {
                var src = $(this).attr('src');
                var $modal = $('.single-gallery__modal');
                var $modalBody = $modal.find('.modal-body');

                $modalBody.html('');
                $modalBody.append($('<img src="' + src + '" class="single-gallery__modal-image">'));
                $modal.modal({});
            });
        })
    </script>
{% endblock %}

Мы используем метод append, который добавляет элемент img в тело модального окна. Поэтому атрибут srcset должен быть там.

Но так как URL-адреса изображений генерируются динамически, то нельзя вызвать фильтр Twig из тега <script>. Можно добавить srcset в миниатюры, а затем использовать его в JavaScript-коде, копируя его из элементов thumb.

Но это не только заставит полноразмерные изображения загружаться на фоне миниатюр (поскольку у нас широкая область просмотра), и вызовет фильтр четыре раза для каждой миниатюры. Это замедлит работу приложения.

Вместо этого создадим новый фильтр Twig в src/Twig/ImageRendererExtension.php. Он будет генерировать полный атрибут srcset для каждого изображения.

public function getImageSrcset(Image $image)
{
    $id = $image->getId();
    $sizes = [1120, 720, 400];
    $string = '';
    foreach ($sizes as $size) {
        $string .= $this->router->generate('image.serve', [
            'id' => $image->getId() . '--' . $size,
        ], RouterInterface::ABSOLUTE_URL).' '.$size.'w, ';
    }
    $string = trim($string, ', ');
    return html_entity_decode($string);
}

Не забываем зарегистрировать данный фильтр:

public function getFilters()
{
    return [
        new Twig_SimpleFilter('getImageUrl', [$this, 'getImageUrl']),
        new Twig_SimpleFilter('getImageSrcset', [$this, 'getImageSrcset']),
    ];
}

Эти значения следует добавить в пользовательский атрибут, который мы будем вызывать из data-srcset для каждой отдельной миниатюры:

<img src="{{ image|getImageUrl(250) }}"
      alt="{{ image.originalFilename }}"
      data-srcset=" {{ image|getImageSrcset }}"
      class="single-gallery__item-image card-img-top">

Теперь у каждой отдельной миниатюры есть атрибут data-srcset с необходимыми значениями srcset. Но такой подход не сработает, поскольку он находится в пользовательском атрибуте, данные в котором будут использоваться позже.

Последний шаг – обновить JavaScript-код, чтобы все заработало.

{% block javascripts %}
    {{ parent() }}

    <script>
        $(function () {
            $('.single-gallery__item-image').on('click', function () {
                var src = $(this).attr('src');
                var srcset = $(this).attr('data-srcset');
                var $modal = $('.single-gallery__modal');
                var $modalBody = $modal.find('.modal-body');

                $modalBody.html('');
                $modalBody.append($('<img src="' + src + '" srcset="" + srcset + '" class="single-gallery__modal-image">'));
                $modal.modal({});
            });
        })
    </script>
{% endblock %}

Добавляем Glide

Glide –библиотека, которая позволяет изменять размер изображений по требованию. Давайте ее установим.

composer require league/glide

Далее регистрируем ее в приложении. Это можно сделать, добавив новую службу в src/Services:

<?php
namespace AppService;

use LeagueGlide;

class GlideServer
{
    private $server;

    public function __construct(FileManager $fm)
    {
        $this->server = $server = GlideServerFactory::create([
            'source' => $fm->getUploadsDirectory(),
            'cache' => $fm->getUploadsDirectory().'/cache',
        ]);
    }

    public function getGlide()
    {
        return $this->server;
    }
}

Данная служба использует сервис FileManager, который внедряется автоматически благодаря новой функции Symfony для автоматического подключения.

Пути входа и выхода объявляем как каталог uploads. Затем добавляем к каталогу выхода суффикс cashe и добавляем метод для получения ответа с сервера. Сервером является экземпляр Glide, который изображение с измененным размером.

Объявим метод getUploadsDirectory как общедоступный:

public function getUploadsDirectory()
{
    return $this->path;

После чего изменим метод serveImageAction в ImageController:

/**
 * @Route("/image/{id}/raw", name="image.serve")
 */
public function serveImageAction(Request $request, $id, GlideServer $glide)
{
    $idFragments = explode('--', $id);
    $id          = $idFragments[0];
    $size        = $idFragments[1] ?? null;

    $image = $this->em->getRepository(Image::class)->find($id);

    if (empty($image)) {
        throw new NotFoundHttpException('Image not found');
    }

    $fullPath = $this->fileManager->getFilePath($image->getFilename());

    if ($size) {

        $info        = pathinfo($fullPath);
        $file        = $info['filename'] . '.' . $info['extension'];
        $newfile     = $info['filename'] . '-' . $size . '.' . $info['extension'];
        $fullPathNew = str_replace($file, $newfile, $fullPath);

        if (file_exists($fullPath) && ! file_exists($fullPathNew)) {

            $fullPath = $fullPathNew;
            $img      = $glide->getGlide()->getImageAsBase64($file,
                ['w' => $size]);

            $ifp = fopen($fullPath, 'wb');

            $data = explode(',', $img);
            fwrite($ifp, base64_decode($data[1]));
            fclose($ifp);
        }
    }

    $response = new BinaryFileResponse($fullPath);
    $response->headers->set('Content-type',
        mime_content_type($fullPath));
    $response->headers->set('Content-Disposition',
        'attachment; filename="' . $image->getOriginalFilename() . '";');

    return $response;
}

Теперь данный метод отделяет идентификатор изображения от размера двойным тире. Как только Doctrine получает путь к изображению из базы данных, размер повторно добавляется к имени файла, если оно было передано. В противном случае используется исходное изображение. Если изображение не существует, оно генерируется из исходного пути и сохраняется для последующего использования.

Создадим файлы вручную, добавляя значения размера и сохраняя изображения в папке uploads.

Также для непосредственного вывода изображения можно использовать метод output Image из библиотеки Glide. В результате изображение будет выводиться прямо из подпапки cache, и не будет сохраняться с суффиксом в основной папке upload.

Но мы реализуем старую логику генерации изображения на основе метода makeImage:

/**
 * @Route("/image/{id}/raw", name="image.serve")
 */
public function serveImageAction(Request $request, $id, GlideServer $glide)
{
    $idFragments = explode('--', $id);
    $id          = $idFragments[0];
    $size        = $idFragments[1] ?? null;

    $image = $this->em->getRepository(Image::class)->find($id);

    if (empty($image)) {
        throw new NotFoundHttpException('Image not found');
    }

    $fullPath = $this->fileManager->getFilePath($image->getFilename());

    if ($size) {

        $info        = pathinfo($fullPath);
        $file        = $info['filename'] . '.' . $info['extension'];

        $cachePath = $glide->getGlide()->makeImage($file, ['w' => $size]);
        $fullPath = str_replace($file, '/cache/' . $cachePath, $fullPath);
    }

    $response = new BinaryFileResponse($fullPath);
    $response->headers->set('Content-type',
        mime_content_type($fullPath));
    $response->headers->set('Content-Disposition',
        'attachment; filename="' . $image->getOriginalFilename() . '";');

    return $response;
}

Мы реализовали изменение размера изображения по требованию. Теперь осталось все протестировать.

Тестирование

Как только мы обновим домашнюю страницу, изображения начнут генерироваться в папке var/uploads. Проверим это.

Теперь у нас есть миниатюра каждого изображения на домашней странице. И это изображение, которое отображается. Обратите внимание на небольшие размеры файлов. Перейдем к галерее и кликнем по изображению, чтобы увидеть его полную версию.

Изображение получено из оригинальной версии. А как же мобильная версия? В современных браузерах достаточно легко включить мобильный режим. Попробуем открыть изображение из галереи в мобильном режиме, а потом проверить папку с изображениями.

А если изменить ориентацию экрана и снова проверить папку?

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

Приложение с изменениями обозначено как этот выпуск.

Заключение

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

Изменять размер изображений по требованию, пока пользователь ожидает – это лучший вариант? Или есть другой, более практичный подход? Узнаем об этом в следующей части.

Данная публикация представляет собой перевод статьи «Improving Performance Perception: On-demand Image Resizing» , подготовленной дружной командой проекта Интернет-технологии.ру

Меню