Доброго времени суток уважаемый читатель. Хотелось бы немного поговорить об оптимизации наших с вам любимых WEB приложений, написанных на нашем горячо любимом и всеми уважаемом фреймворке Django. В частности речь в этой статье пойдёт об оптимизации изображений. А теперь по порядку.
А что там Google Lighthouse?
Если вы хоть раз нажимали правой кнопкой мыши на экране вкладки, открытой в Google Chrome, а затем щёлкали "Просмотреть код", то Вы могли видеть инструмент для анализа вашего приложение под названием Lighthouse:

Проведя тест Вашего приложения вы можете увидеть следующее:

Ссылка Learn more about modern image formats. ведёт на эту страницу. Если коротко, то нам предлагают избавится от изображений в старых форматах и приобщиться к более современным, в частности к формату webp. Более подробно вы можете ознакомится самостоятельно, перейдя по ссылке.
Django, ты как там?
Коль скоро речь заходит о хранении изображений на стороне Back-end, то мы не будем томить и приведём пример хранения изображения в нашей базе данных, допустим у нас будет логотип компании, который нам необходимо сохранить в формате webp. Для этого напишем в файле models.py следующее:
class CompanyLogo(models.Model): logo = models.ImageField( upload_to='images/logo', verbose_name='Лого' )
А чего тут нового, спросите вы и будете абсолютно правы. Это вполне себе стандартный синтаксис модели Django. Давайте напишем функцию, которая на входе принимает файл изображения, допустим jpeg или png, а возвращает изображение в формате webp, да ещё и подрезанное, как того требует макет скажем в Figma. По желанию, вы можете хранить этот код в отдельном файле проекта и импортировать в случае необходимости, я же добавлю эту функцию в модель CompanyLogo.
from django.db import models from io import BytesIO from PIL import Image from django.core.files import File from django.core.files.base import ContentFile class CompanyLogo(models.Model): logo = models.ImageField( upload_to='images/logo', verbose_name='Лого' ) def compress_logo(self, image): im = Image.open(image) width, height = im.size[0], int(im.size[0] * 1.5) x, y = 0, int((im.size[1] - height) // 2) area = (x, y, x+width, y+height) im = im.crop((area)) im = im.resize((200, 300)) im_bytes = BytesIO() im.save(fp=im_bytes, format="WEBP", quality=100) image_content_file = ContentFile(content=im_bytes.getvalue()) name = image.name.split('.')[0] + '.WEBP' new_image = File(image_content_file, name=name) return new_image
Следует упомянуть, что для работы с полем ImageField в Django, требуется модуль Pillow, которой нужно предварительно установить в нашу виртуальную среду pip install Pillow.
Наверное надо немного пояснить, что конкретно делает этот код. Он принимает на входе загруженное изображение, затем берёт за основу его ширину и вычисляет требуемую высоту, пропорционально соотношению сторон (300/200=1.5). Это сделано для того, что бы загрузив даже квадратное изображение вы получили на выходе прямоугольное без потери качества и искажения. Затем мы обрезаем изображение и сжимаем его в соответствии с параметрами макета. Выбираем формат выходного изображения и его качество. Получаем байтовое значение файла. Присваиваем ему имя и сохраняем файл. Вот что получается:

Функция написана, теперь надо понять как и когда ее вызвать. И для этого нам потребуется переопределить метод save класса модели, а именно:
def save(self, *args, **kwargs): try: this = CompanyLogo.objects.get(id=self.id) if this.logo != self.logo: this.logo.delete(save=False) try: new_logo = self.compress_logo(self.logo) self.logo = new_logo super(CompanyLogo, self).save(*args, **kwargs) except ValueError: super(CompanyLogo, self).save(*args, **kwargs) else: super(CompanyLogo, self).save(*args, **kwargs) except CompanyLogo.DoesNotExist: try: new_logo = self.compress_logo(self.logo) self.logo = new_logo super(CompanyLogo, self).save(*args, **kwargs) except ValueError: super(CompanyLogo, self).save(*args, **kwargs)
Возможно код выглядит немного громоздко, но он работает, если у кого то будут идеи, как его переписать более лаконично я буду этому рад.
Так, что же делает этот код, по мимо того, что вызывает нашу функцию для сжатия изображения при сохранении записи в базе данных? Он позволяет при обновлении или удалении файла (допустим через Django admin), а так же при удалении записи из базы данных удалить физический файл с сервера (ведь все мы стараемся сберечь место на SSD), и если предположить, что у вас очень большой проект, который может содержать огромное количество подобных изображений, то момент с удалением ненужных файлов с сервера будет весьма и весьма полезным.
Что в итоге?
Тоже самое изображение, обработанное по алгоритму из функции compress_logoно имеющее разрешение png занимает на диске примерно 12,2 кб., когда как изображение в формате webp занимает всего 6 кб.

Вам может показаться, что в реальных цифрах разница не так велика, и вы подумаете, что экономите всего 6 килобайт, но на деле экономия чуть больше 50% процентов, согласитесь не плохо? А если представить, что вы разрабатываете крупную площадки по размещению объявлений, то реальная экономия может оказаться очень весомой.
Надеюсь, этот материал окажется для кого-то полезным. Спасибо, что дочитали текст до конца!
