python

Делаем адаптивную загрузку контента на сайте

  • среда, 18 декабря 2019 г. в 00:35:36
https://habr.com/ru/post/480704/
  • Разработка веб-сайтов
  • Python
  • JavaScript
  • ReactJS


Привет, Хабр!

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

Всех, кому интересно, как я разобрался с этой проблемой, прошу под кат.

Собственно, проблема


Как я уже сказал, на сайте ОЧЕНЬ много гифок, хороших и разных. Можете сами посмотреть, сколько их: reface.tech

image

Как только я напилил бета-версию лендинга, сразу возникла проблема с загрузкой: у некоторых пользователей страницы долго грузились. Для сурьезного бизнеса, это, конечно, не дело.

Надо было это как-то решать. Простым lossless сжатием обойтись было нельзя, так уже на тот момент весь медиа-контент уже был прогнан через compressor.io. Следовательно, надо было сжимать с потерями и выдавать гифки с худшим качеством.

Но выдавать пользователям с хорошим интернетом плохой медиаконтент — это какое-то кощунство. Поэтому решено было как-то определять скорость этих ваших интернетов, и в зависимости от скорости подсовывать соответствующее качество.

Делаем примерный набросок


У нас будет массив с описанием медиаконтента, который нужно грузить.

Пример:

 [
            {
                "large": {
                    "size": 0.6211118698120117,
                    "url": "gifs/control/large/control_1.gif"
                },
                "middle": {
                    "size": 0.5330495834350586,
                    "url": "gifs/control/middle/control_1.gif"
                },
                "small": {
                    "size": 0.4901447296142578,
                    "url": "gifs/control/small/control_1.gif"
                }
            }
]

Тут у нас в элементике для соответствующей степени качества лежит урл и размер файла.

Мы будем просто:

  1. Проходиться по массиву (при загрузке страницы)
  2. Считать суммарный размер подгружаемого для каждой степени качества
  3. Определять, подгрузится ли это все за 4 секунды (это срок, который лично меня устривает)

Соответственно, надо:

  1. Напилить какую-то штуку, которая будет делать такой массив, чтобы все ручками не считать (на питоне)
  2. Напилить определитель скорости интернетов

Пилим определитель скорости


Это будет сравнительно просто. Набираем в адресной строке вашего браузера eu.httpbin.org/stream-bytes/51200. Скачается файлик длиной в 51200 байтов. Пихаем его себе в public (чтобы измерять скорость до хостинга).

Теперь нам надо проверить, за сколько файлик качается. Напилим для этого простую функцию, которая нам будет возвращать скорость в мегабайтах в секунду.

async checkDownloadSpeed(baseUrl, fileSizeInBytes) {
        return new Promise((resolve, _) => {
            let startTime = new Date().getTime();

            return axios.get(baseUrl).then( response => {
                const endTime = new Date().getTime();
                const duration = (endTime - startTime) / 1000;

                const bytesPerSecond = (fileSizeInBytes / duration);
                const megabytesPerSecond = (bytesPerSecond / 1000 / 1000);

                resolve(megabytesPerSecond);
            });
        }).catch(error => {
            throw new Error(error);
        });
}

То есть мы засекли время начала подгрузки, время окончания, измерили разницу, и, так как мы знаем размер файла, просто поделили.

Теперь напишем функцию, которая будет что-то делать с этой определенной скоростью:

async getNetworkDownloadSpeed() {
        const baseUrl = process.env.PUBLIC_URL + '/51200';
        const fileSize = 51200;

        const speed = await this.checkDownloadSpeed(baseUrl, fileSize);
        console.log("Network speed: " + speed);

        if (speed.mbps === "Infinity") {
            SpeedMeasure.speed = 1;
        }

        else {
            SpeedMeasure.speed = speed * 5;
        }
}

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

Теперь напилим функцию, которая в зависимости от скорости будет подсовывать соответствующее качество:

 static getResolution(gifsArray) {
        let totalSizeLevel1 = 0;
        let totalSizeLevel2 = 0;
        let totalSizeLevel3 = 0;

        for (let i = 0; i < gifsArray.length; i++) {
            for (let a = 0; a < gifsArray[i].length; a++) {
                let element = gifsArray[i][a];

                totalSizeLevel1 += element.small.size;
                totalSizeLevel2 += element.middle.size;
                totalSizeLevel3 += element.large.size;
            }
        }

        if (isNaN(SpeedMeasure.speed)) {
            SpeedMeasure.speed = 1;
        }

        let timeLevel1 = totalSizeLevel1 / SpeedMeasure.speed;
        let timeLevel2 = totalSizeLevel2 / SpeedMeasure.speed;
        let timeLevel3 = totalSizeLevel3 / SpeedMeasure.speed;

        if (timeLevel3 < APPROPRIATE_TIME_LIMIT) {
            return "large";
        }

        else if (timeLevel2 < APPROPRIATE_TIME_LIMIT) {
            return "middle";
        }

        else {
            return "small";
        }
}

Так как функция, которая нам считает скорость, асинхронная, SpeedMeasure.speed может быть нулевым. По дефолту считаем, что скорость соединения 1 мегабайт в секунду. Когда функция посчитает скорость, мы просто перерендерим контейнер.

Мы передаем в функцию getResolution массив массивов. Почему? Потому что если у нас на странице несколько контейнеров, нам удобнее в каждый из них передавать соответствующий контент массивом, а вот считать скорость подгрузки надо для всех разом.

Пример использования


Вот пример использования (на React'e):

    async runFunction() {
        let speedMeasure = new SpeedMeasure();
        await speedMeasure.getNetworkDownloadSpeed();
        this.forceUpdate()
    }

    componentDidMount() {
        this.runFunction();
    }

    render() {
        let quality = SpeedMeasure.getResolution([
            Control.getControlArray(),
            Health.getHealthArray()
        ]);

        return (
            <div className="app">
                <Presentation />
                <Control quality={quality} />
                <Health quality={quality} />
            </div>
        );
}

Соответственно, когда у нас догрузится определение скорости, контейнер снова будет отрендерен.
Внутри контейнера (например, внутри Control) вы просто берете соответствующую гифку из массива (по индексу), потом по quality получаете объектик и по url получаете ссылочку. Все просто.

Пишем скриптик на питоне


Теперь нам нужно как-то автоматически сжимать гифки и аутпутить массив с описанием контента.

Для начала напишем скриптик для сжатия. Будем использовать gifsicle. Для контента middle-компрессии уровень компрессии будет 80 (из 200), для совсем мощной штучки будет 160.

import os

GIFS_DIR = "/home/mixeden/Документы/Landingv2/"
COMPRESSOR_DIR = "/home/mixeden/Документы/gifsicle-static"

NOT_OPTIMIZED_DIR = "not_optimized"
OPTIMIZED_DIR = "optimized"
GIF_RESIZED_DIR = "gif_not_optimized_resized"
GIF_COMPRESSED_DIR = "gif_compressed"
COMPRESSION_TYPE = ["middle", "small"]

for (root, dirs, files) in os.walk(GIFS_DIR, topdown=True):
    if len(files) > 0 and GIF_RESIZED_DIR in root:
        for file in files:
            path = root + "/" + file

            for compression in COMPRESSION_TYPE:
                final_path = path.replace(GIF_RESIZED_DIR,
                                          GIF_COMPRESSED_DIR + "/" + compression + "/" + OPTIMIZED_DIR)
                print(path, final_path)

                if compression == COMPRESSION_TYPE[0]:
                    rate = 80

                else:
                    rate = 160

                os.system("echo 0 > " + final_path)
                os.system(COMPRESSOR_DIR + " -O3 --lossy={} -o {} {}".format(rate, final_path, path))

Чтобы были понятны пути до файликов, вот описание файловой директории с гифками:

  1. NOT_OPTIMIZED_DIR — неоптимизированные гифки
  2. GIF_RESIZED_DIR — неоптимизированные гифки, но отресайзенные в соответствии с размерами контейнера под них
  3. GIF_COMPRESSED_DIR — сжатые гифки

Внутри папок есть папки с названиями категории гифок. Внутри папки с определенной содержатся папки «large», «middle» и «small» (под виды компресснутых файлов).

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

Теперь перейдем к скрипту для создания массива с информацией.

import json
import os

GIFS_DIR = "/home/mixeden/Документы/Landingv2/"
COMPRESSOR_DIR = "/home/mixeden/Документы/gifsicle-static"

NOT_OPTIMIZED_DIR = "not_optimized"
OPTIMIZED_DIR = "optimized"
GIF_RESIZED_DIR = "gif_not_optimized_resized"
GIF_COMPRESSED_DIR = "gif_compressed"
COMPRESSION_TYPE = ["large", "middle", "small"]

OUTPUT = {}

for (root, dirs, files) in os.walk(GIFS_DIR, topdown=True):
    if len(files) > 0 and GIF_COMPRESSED_DIR in root and NOT_OPTIMIZED_DIR not in root:
        files.sort()

        type = root.split(GIFS_DIR)[1].split(GIF_COMPRESSED_DIR)[0].replace("/", "")
        print(type)

        if type not in OUTPUT:
            OUTPUT[type] = []

        if len(OUTPUT[type]) == 0:
            for file in files:
                OUTPUT[type].append(
                    {
                        "large": {
                            "url": "",
                            "size": 0
                        },
                        "middle": {
                            "url": "",
                            "size": 0
                        },
                        "small": {
                            "url": "",
                            "size": 0
                        }
                    })

        for file in files:
            full_path = root + "/" + file
            bytes_size = os.path.getsize(full_path)
            kilobytes_size = bytes_size / 1000
            megabytes_size = kilobytes_size / 1000

            index = int(file.split("_")[1].replace(".gif", "")) - 1

            for typer in COMPRESSION_TYPE:
                if typer in root:
                    local_type = typer
                    
            new_url = "gifs/" + full_path.replace(GIFS_DIR, "").replace("/" + GIF_COMPRESSED_DIR, "").replace("/" + OPTIMIZED_DIR, "")

            OUTPUT[type][index][local_type]['url'] = new_url
            OUTPUT[type][index][local_type]['size'] = megabytes_size

print(OUTPUT)
print(json.dumps(OUTPUT, indent=4, sort_keys=True))

Тут мы проходимся по папочкам с файлами, определяем размер файла, выясняем вид компрессии и кладем общую информацию в массив OUTPUT. А потом красиво выводим этот массив в консоль, чтобы его скопировать.

Заключение


Надеюсь, эта статья вам чем-то поможет. Всем приятного кодинга, ребята.