Я хочу, чтобы посетители моего сайта наслаждались им, так что я забочусь об accessibility и проверяю, что даже без JavaScript тут есть, на что смотреть. Я забочусь о том, насколько быстро грузятся страницы, ведь на некоторых из них есть большие иллюстрации, поэтому я минифицирую HTML.

Вот только есть один нюанс, который ставит мне палки в колёса и не даёт сделать блог лёгким как пёрышко.

Палка

Наиболее сильно уменьшает трафик (а значит и latency на мобильных устройствах!) не минификация, а сжатие. HTTP поддерживает gzip и Brotli через заголовок Content-Encoding. Сжатие отнимает ресурсы сервера, поэтому оно не всегда применяется, ведь отправка несжатых данных банально может быть быстрее.

Как правило, Brotli лучше, чем gzip, а gzip лучше, чем ничего. gzip настолько малозатратен, что он на серверах по умолчанию включен, а вот Brotli на порядок медленнее.

К несчастью, мой блог хостится на GitHub pages, который не поддерживает Brotli. Из-за этого Recovering garbled Bitcoin addresses, самый длинный пост у меня на сайте, занимает 92 килобайта вместо 37.

Лишний трафик, в 2.5 раза больше, чем нужно.

Неохота думать...

Нет причины, по которой GitHub не может поддерживать Brotli. Даже если сжатие файлов на лету не самое быстрое, GitHub мог бы дать владельцам репозиториев возможность заливать предварительно сжатые данные и их использовать.

GitHub, конечно же, этого не делает, но заранее сжатые данные можно просто положить в репу. Другое дело, что придётся руками разжимать их JS-ом на клиенте.

Как крутая разработчица, первым делом я пошла за решением проблемы в гугл. Быстрым поиском выловился brotli-dec-wasm — декомпрессор на WASM, укладывающийся в 200 килобайт. tiny-brotli-dec-wasm почти втрое меньше: 71 килобайт.

Ага, то есть имеем 92 килобайта с gzip против 37 + 71 килобайт с Brotli. Ну такоооооее....

Те же грабли

Так, а с чего это WASM вообще нужен? В браузере же должен быть декодер Brotli в HTTP-стеке. Неужели никакой APIшки нет?

Конечно же есть — Compression Streams API. Например, конструктор DecompressionStream принимает аргумент format, задокументированный как:

One of the following compression formats:

  • "gzip"

    Decompress the stream using the GZIP format.

  • "deflate"

    Decompress the stream using the DEFLATE algorithm in ZLIB Compressed Data Format. The ZLIB format includes a header with information about the compression method and the uncompressed size of the data, and a trailing checksum for verifying the integrity of the data

  • "deflate-raw"

    Decompress the stream using the DEFLATE algorithm without a header and trailing checksum.

Хорошо, а где Brotli? А, его просто не добавили. Надеюсь, что вскоре с мёртвой точки сдвинутся, но все мы знаем, с какой медлительностью продвигаются такие вещи.

У меня в голове промелькнула мысль использовать gzip, но предварительное сжатие более эффективной библиотекой Zopfli выдаёт файл весом 86 килобайт, что всё ещё заметно хуже Brotli.

Во все тяжкие

У меня уже начали опускаться руки, но внезапно меня озарила демосценерская мудрость.

Браузеры умеют декодить картинки. Если положить данные в картинку и забрать их через Canvas API, и если сжатие без потерь и достаточно эффективное, будет профит.

Надеюсь, вы понимаете, к чему я тут клоню, и орёте "Да ты совсем рехнулась!" в монитор.

Простейший формат изображений со сжатием без потерь — GIF. GIF сканирует картинку по строкам и применяет к полученным данным LZW — алгоритм, которому сто лет в обед (1984). DEFLATE, используемый в gzip, придумали как раз на замену LZW, так что гифки тут не к месту.

PNG тоже использует DEFLATE, но, что важно, перед этим прогоняет данные через дополнительное преобразование. DEFLATE применяется не к сырым пикселям, а к разнице между соседними пикселями, например, к [a, b-a, c-b, d-c] вместо [a, b, c, d]. (Есть ещё другие, более изощрённые преобразования.) Это делает PNG предиктивным форматом: вместо сырых данных хранится разница от предсказания ("ошибка"), которая во многих случаях достаточно мала (ура, асимметричные вероятности, Хаффману заходит).

Ну только не это!

Победитель тут, несомненно, WebP, формат, который половина фанатиков нарекает исчадием ада, а другая — даром свыше. У WebP есть два варианта: с потерями и без, — использующие сильно отличающиеся алгоритмы. Речь тут пойдёт о VP8L, формате без потерь.

VP8L похож на PNG: он тоже использует предиктивное преобразование (чуть покруче, чем в PNG), но куда важнее то, что Google заменил DEFLATE на похожий самопальный формат.

DEFLATE позволяет нарезать файл на куски и использовать отдельные деревья Хаффмана для каждого куска. Оно и понятно: обычно данные не однородны, и у разных частей данных разные частоты встречаемости символов и ссылок на прошлые пиксели. Таким образом, JavaScript, SVG и разметка в одном HTML файле, скорее всего, будут использовать разные деревья.

В VP8L это тоже поддерживается, но со своей изюминкой: WebP позволяет заранее объявить сколько угодно различных деревьев Хаффмана и использовать своё дерево для каждого блока пикселей 16x16. Это важно, потому что позволяет переиспользовать деревья. То есть пока DEFLATE кодирует последовательность "JavaScript, CSS, потом опять JavaScript" тремя деревьями, хотя первое и третье из них очень похожи, VP8L спокойно обходится двумя. А ещё это улучшает локальность, потому что часто переключать деревья так дешевле.

Больше прекрасностей

Ещё одна крутая фича VP8L — "color cache". Вот наглядная демонстрация похожей техники:

Представьте, что вы разрабатываете очень тупое сжатие JSON. Вы хотите эффективно кодировать специльные символы: ", [, ], {, }, и прочие. Часто сказать "этот символ — маркер" достаточно, чтобы однозначно его восстановить. Например, в "s<MARKER> маркер совершенно точно ", а в [1, 2, 3<MARKER> это, очевидно, ].

Тут похожая идея: иногда вместо того, чтобы хранить весь пиксель, достаточно запомнить, что нам нужна копия последнего встреченного пикселя с определённым свойством (например, шестибитным хешом).

Поехали крышей!

В качестве бенчмарка я пока что возьму статью Recovering garbled Bitcoin addresses.

$ curl https://purplesyringa.moe/blog/recovering-garbled-bitcoin-addresses/ -o test.html

$ wc -c test.html
439478 test.html

$ gzip --best <test.html | wc -c
94683

Ок. Теперь по-быстрому протестим сжатие крейтом webp.

$ cargo add webp
fn main() {
    let binary_data = include_bytes!("../test.html");

    // Эээ... 1xN?
    let width = binary_data.len() as u32;
    let height = 1;

    // Перевод в оттенки серого
    let mut image_data: Vec<u8> = binary_data.iter().flat_map(|&b| [b, b, b]).collect();

    // Без потерь, качество 100 (лучшее сжатие)
    let compressed = webp::Encoder::from_rgb(&image_data, width, height)
        .encode_simple(true, 100.0)
        .expect("encoding failed");

    println!("Data length: {}", compressed.len());
    std::fs::write("compressed.webp", compressed.as_ref()).expect("failed to write");
}

Почему чёрно-белая картинка? WebP поддерживает преобразование "subtract green", при котором перед кодированием битмапов значение зелёного канала вычитается из красного и синего. В чёрно-белых изображениях это фактически обнуляет каналы R и B. WebP кодирует разные каналы разными деревьями Хаффмана, поэтому на однотонные каналы тратится O(1) места.

$ cargo run
thread 'main' panicked at src/main.rs:13:100:
encoding failed: VP8_ENC_ERROR_BAD_DIMENSION
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

Ой... кажется, WebP умеет только в картинки до 16383x16383. Дадим ему такую.

fn main() {
    let binary_data = include_bytes!("../test.html");

    // Эээ... 16383xN?
    let width = 16383;
    let height = (binary_data.len() as u32).div_ceil(width);

    // Перевод в оттенки серого, дополняя данные до размера картинки
    let mut image_data: Vec<u8> = binary_data.iter().flat_map(|&b| [b, b, b]).collect();
    image_data.resize((width * height * 3) as usize, 0);

    // Без потерь, качество 100 (лучшее сжатие)
    let compressed = webp::Encoder::from_rgb(&image_data, width, height)
        .encode_simple(true, 100.0)
        .expect("encoding failed");

    println!("Data length: {}", compressed.len());
    std::fs::write("compressed.webp", &*compressed).expect("failed to write");
}
$ cargo run
Data length: 45604

Неплохо. Это уже в два раза меньше чем gzip, и даже круче bzip2 (49764 байт)!

Подгоны

Но мы можем сделать ещё лучше, если воспользуемся особенностями конкретно формата WebP.

Например, при использовании широкой картинки с порядком "по строкам" в блоках 16x16 оказываются байты, лежащие в файле далеко друг от друга: первые 16 пикселей берутся из первых 16 килобайт, вторые — из следующих, и так далее. Как насчёт высокой картинки?

// Эээ... Nx16383?
let height = 16383;
let width = (binary_data.len() as u32).div_ceil(height);
println!("Using {width}x{height}");
$ cargo run
Using 27x16383
Data length: 43232

Библиотека cwebp позволяет влиять на эффективность сжатия не только за счёт качества, но и заменой "метода". Пробуем:

// Без потерь, качество 100
let mut config = webp::WebPConfig::new().unwrap();
config.lossless = 1;
config.quality = 100.0;

for method in 0..=6 {
    // Пробуем разные "методы" (4 -- значение по умолчанию)
    config.method = method;
    let compressed = webp::Encoder::from_rgb(&image_data, width, height)
        .encode_advanced(&config)
        .expect("encoding failed");

    println!("Method {method}, data length: {}", compressed.len());
}
$ cargo run
Method 0, data length: 48902
Method 1, data length: 43546
Method 2, data length: 43442
Method 3, data length: 43292
Method 4, data length: 43232
Method 5, data length: 43182
Method 6, data length: 43182

Возьмём метод 5: он, кажется, не хуже чем 6, но быстрее.

Мы уже в 2.2 раза круче gzip, и всего в 1.2 раза хуже Brotli — для наших условий очень даже неплохо.

Бенчмарки

Давайте чисто по приколу протестируем наш формат на разных файлах. Я буду использовать тестовые данные snappy, Canterbury Corpus и Large Corpus, и две больших SVG-шки.

Для начала перепишу скрипт, чтобы его можно было засунуть в пайплайн:

use std::io::{Read, Write};

fn main() {
    let mut binary_data = Vec::new();
    std::io::stdin().read_to_end(&mut binary_data).expect("failed to read stdin");

    let width = (binary_data.len() as u32).div_ceil(16383);
    let height = (binary_data.len() as u32).div_ceil(width);

    // Перевод в оттенки серого, дополняя данные до размера картинки
    let mut image_data: Vec<u8> = binary_data.iter().flat_map(|&b| [b, b, b]).collect();
    image_data.resize((width * height * 3) as usize, 0);

    // Без потерь, качество 100, метод 5
    let mut config = webp::WebPConfig::new().unwrap();
    config.lossless = 1;
    config.quality = 100.0;
    config.method = 5;
    let compressed = webp::Encoder::from_rgb(&image_data, width, height)
        .encode_advanced(&config)
        .expect("encoding failed");

    std::io::stdout().write_all(&compressed).expect("failed to write to stdout");
}

(Ещё я слегка поменяла расчёт длины, чтобы он работал с разными размерами файлов.)

А теперь пришло время сравнить gzip, bzip2, brotli и webp на нашем корпусе:

#!/usr/bin/env bash
cd corpus

printf "%24s%8s%8s%8s%8s%8s\n" File Raw gzip brotli bzip2 webp
for file in *; do
    printf \
        "%24s%8d%8d%8d%8d%8d\n" \
        "$file" \
        $(<"$file" wc -c) \
        $(gzip --best <"$file" | wc -c) \
        $(brotli --best <"$file" | wc -c) \
        $(bzip2 --best <"$file" | wc -c) \
        $(../compressor/target/release/compressor <"$file" | wc -c)
done

Файл

Без сжатия

gzip

brotli

bzip2

webp

AJ_Digital_Camera.svg

132619

28938

22265

27113

26050

alice29.txt

152089

54179

46487

43202

52330

asyoulik.txt

125179

48816

42712

39569

47486

bible.txt

4047392

1176635

889339

845635

1101200

cp.html

24603

7973

6894

7624

7866

displayWebStats.svg

85737

16707

10322

16539

14586

E.coli

4638690

1299059

1137858

1251004

1172912

fields.c

11150

3127

2717

3039

3114

fireworks.jpeg

123093

122927

123098

123118

122434

geo.protodata

118588

15099

11748

14560

13740

grammar.lsp

3721

1234

1124

1283

1236

html

102400

13584

11435

12570

12970

html_x_4

409600

52925

11393

16680

13538

kennedy.xls

1029744

209721

61498

130280

212620

kppkn.gtb

184320

37623

27306

36351

36754

lcet10.txt

426754

144418

113416

107706

134670

paper-100k.pdf

102400

81196

80772

82980

81202

plrabn12.txt

481861

194264

163267

145577

186874

ptt5

513216

52377

40939

49759

49372

sum

38240

12768

10144

12909

12378

urls.10K

702087

220198

147087

164887

170052

world192.txt

2473400

721400

474913

489583

601188

xargs.1

4227

1748

1464

1762

1750

Страшная таблица. Наглядно:

Диаграмма степеней сжатия
Диаграмма степеней сжатия

Сразу видно, что WebP почти всегда лучше gzip, кроме очень маленьких файлов (grammar.lsp и xargs.1), и ещё вот этих двух:

Файл

Без сжатия

gzip

brotli

bzip2

webp

kennedy.xls

1029744

209721

61498

130280

212620

paper-100k.pdf

102400

81196

80772

82980

81202

paper-100k.pdf — практически шум (в файле 19 килобайт XML, после чего куча уже сжатых данных, так что по факту мы тут уже измеряем сжатие маленьких файлов).

Сложно сказать, что не так с kennedy.xls. Ещё на этом файле очень странные относительные скорости у Brotli и bzip2. Я думаю, это потому, что в этом файле идёт подряд много разнородной информации, что оказывается слишком сложным для алгоритмов сжатия.

WebP работает в среднем чуть хуже bzip2: он обгоняет его в нескольких отдельных случаях и сливается в куче других. Оно и неудивительно: используются сильно разные алгоритмы, дающие разные результаты на разных данных.

Также ожидаемо, что WebP оказывается всегда хуже Brotli (кроме файла fireworks.jpeg с белым шумом, где звёзды решили сойтись). Тем не менее, WebP заметно лучше gzip на больших массивах текста, в том числе на SVG-шках, и больше всего на html_x_4, где он выдаёт степень сжатия 3.3% (хуже чем Brotli с его 2.8%, но куда лучше 13% от gzip).

В целом, кажется, WebP — неплохое решение для Веба.

JavaScript

С теорией разобрались, перейдём к "практическим" аспектам кодирования и декодирования.

WebP можно без особых проблем раскодировать через Canvas API:

<script type="module">
// Загружаем файл WebP
const result = await fetch("compressor/compressed.webp");
const blob = await result.blob();

// Раскодируем в RGBA
const bitmap = await createImageBitmap(blob);
const context = new OffscreenCanvas(bitmap.width, bitmap.height).getContext("2d");
context.drawImage(bitmap, 0, 0);
const pixels = context.getImageData(0, 0, bitmap.width, bitmap.height).data;

// Достаём из красного канала сырые байты HTML
const bytes = new Uint8Array(bitmap.width * bitmap.height);
for (let i = 0; i < bytes.length; i++) {
    bytes[i] = pixels[i * 4];
}

// Осталось декодировать UTF-8
const html = new TextDecoder().decode(bytes);

document.documentElement.innerHTML = html;
</script>

...в параллельной вселенной. Canvas API — ходовой инструмент фингерпринтинга, поэтому браузеры подложили нам свинью и гадят мусором в данные, которые возвращает getImageData.

Эти изменения почти незаметны. Если пройти по этой ссылке в Firefox со включённой "строгой" защитой от отслеживания, можно заметить, что заменяется меньше 1% пикселей. На практике это выглядит как опечатки в HTML, и я сначала подумала, что они настоящие.

Я презираю эту "защиту приватности". Мало того, что она ломает реальные юзкейсы (декодирование WebP никак не может зависеть от устройства), но оно ещё и бесполезно, поскольку добавление характерного (!) шума увеличивает, а не уменьшает уникальность отпечатков.

Я не понимаю почему, но если использовать WebGL, всё работает:

const bitmap = await createImageBitmap(blob);
const context = new OffscreenCanvas(bitmap.width, bitmap.height).getContext("webgl");
const texture = context.createTexture();
context.bindTexture(context.TEXTURE_2D, texture);
context.texImage2D(context.TEXTURE_2D, 0, context.RGBA, context.RGBA, context.UNSIGNED_BYTE, bitmap);
context.bindFramebuffer(context.FRAMEBUFFER, context.createFramebuffer());
context.framebufferTexture2D(context.FRAMEBUFFER, context.COLOR_ATTACHMENT0, context.TEXTURE_2D, texture, 0);
const pixels = new Uint8Array(bitmap.width * bitmap.height * 4);
context.readPixels(0, 0, bitmap.width, bitmap.height, context.RGBA, context.UNSIGNED_BYTE, pixels);
// Смотрите, никакого шума!

Нет, я не знаю, отчего readPixels не подвержен такому же анти-фингерпринтингу. Но он не создаёт по всей странице едва заметные опечатки, так что будем считать что это работает.

WebGL гарантированно поддерживает только текстуры до 2048x2048, так что некоторые ограничения придётся обновить.

В минифицированном виде этот код занимает где-то 550 байт. Вместе с самой картинкой WebP получается размер в 44 килобайта (Для сравнения: gzip сжал бы в 92 килобайта, а Brotli в 37).

Выдраивание

Вот чем мне не нравится это решение — так это долбаным мерцанием.

Поскольку await автоматически транслируется в код на промисах, браузер считает работу скрипта оконченной до того, как WebP загрузился. В DOM пока ничего нет, так что браузер ничтоже сумняшеся рисует пустую белую страницу.

Через сотню-другую миллисекунд, когда WebP таки подгружается, происходит парсинг HTML, загрузка CSS и вычисление стилей, правильный DOM отрисовывается на экране, заменяя собой пустоту.

Это можно очень просто исправить: достаточно положить стили и верхнюю часть страницы (примерно 8 килобайт в сжатом виде) в gzip'нутый HTML и сжимать через WebP только то, что находится за границей viewport'а. Перезагрузка страницы где-то внизу всё равно будет выглядеть по-наркомански, но этим хотя бы можно пользоваться.

Ещё одна неприятность есть с прокруткой. Обычно при обновлении страницы состояние скролла сохраняется, но теперь, если вы находитесь на Y = 5000px и обновите страницу, браузер загрузит страницу высотой 0px и позиция собьётся. Это можно исправить, если временно добавить очень высокий <div>. При этом важно использовать именно document.documentElement.innerHtml, а не document.write, потому что так можно обновить текущий документ, а не заменить его новым.

Встраивание

Наконец, попробуем ещё немного уменьшить задержку. Для этого встроим WebP прямо в HTML.

Самый простой способ это сделать — использовать data URL в формате base64. Но разве это не увеличит размер файла на треть? Да, увеличит, но gzip это увеличение практически полностью скомпенсирует:

$ wc -c compressed.webp 
43182 compressed.webp

$ base64 -w0 compressed.webp | wc -c
57576

$ base64 -w0 compressed.webp | gzip --best | wc -c
43519

Почему? Ну, поскольку WebP — сжатый файл, его можно считать белым шумом, и это свойство сохраняется после прогона base64, переводящего восемь бит в шесть. Дерево Хаффмана, полученное при применении gzip на белом шуме, фактически производит обратное преобразование из шестибитного формата в восьмибитный.

Можно было бы использовать Unicode и UTF-16 вместо base64, но иногда правильное решение приходит в голову первым.

Пример

(Прим. пер.: речь идёт об оригинальной статье на английском. Хабр не поддерживает выполнение JavaScript в статьях, поэтому этот перевод через WebP не закодирован. А жаль.)

Реальная веб-страница, сжатая через WebP? Как насчёт той, которую вы читаете прямо сейчас? Если только у вас не старый браузер или отключён JavaScript, всё содержимое, начиная с раздела "Те же грабли", было сжато через WebP. Если вы этого не заметили, значит мой трюк работает :-)

А, кстати, хотите посмотреть на этот WebP? Вот квадратный WebP с содержимым этой страницы:

Эта страница в формате WebP
Эта страница в формате WebP

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

Светлая часть вверху и в самом низу — текст и код. Полосатая часть на 20% по высоте — диаграмма. Тёмная часть, занимающая больше всего места — текст на диаграмме (да, там не используется шрифт).

Несколько ярких пикселей среди текста? Это символы Юникода, в основном знаки препинания вроде апострофа и троеточия.

На самом деле, сэкономили мы тут только на спичках: исходная версия, сжатая через gzip, занимает 88 килобайт, а версия, сжатая через WebP и gzip — 83 килобайта. При этом Brotli выдал бы 69 килобайт. Всё лучше чем ничего.

Ну и блин, прикольно же. Мне нравится прикалываться!

Ссылки

Код на Rust, корпус и некоторые другие файлы доступны на GitHub.
Если хотите, можете присоединиться к обсуждению на Reddit или на Hacker News.

Комментарии (52)


  1. saboteur_kiev
    08.09.2024 20:55
    +1

    Нет причины, по которой GitHub не может поддерживать Brotli

    Как правило, Brotli лучше, чем gzip, а gzip лучше, чем ничего. gzip настолько малозатратен, что он на серверах по умолчанию включен, а вот Brotli на порядок медленнее.

    Вы же сами ответили на свой вопрос.
    Гитхаб это не обычный веб-сайт, это сервис, в котором исходники часто отдают по https а не ssh, и гитхаб могут использовать для различных активностей, автоматизации, даже синхронизации конфигов с различных устройств, в том числе и смарт-устройств, работающих на 1-5 ватт, где процессор может быть запитан от маломощной батарейки.

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


    1. purplesyringa
      08.09.2024 20:55
      +4

      Речь о GitHub Pages, который для исходников, конфигов и прочего (обычно) не используется.

      HTTP-клиент передает допустимые форматы в загаловке Accept-Encoding, сервер далее имеет право сжимать только каким-либо из этих форматов. Проблем с совместимостью благодаря этому быть не должно.


      1. Balling
        08.09.2024 20:55

        Это вообще бред, что вы несете. Да если бы youtube приложение/chrome не использовало gzip там было бы 6 мбайт данных (не мбит) в секунду на одном тексте... Так и есть в ffmpeg/mpv, так как там gzip и br не поддерживается. https://github.com/mpv-player/mpv/issues/14360

        После сжатия 300 кбайт.


        1. mayorovp
          08.09.2024 20:55
          +2

          Не вижу в вашем комментарии логики. Откуда вообще взялся Ютуб и почему вы вообще рассматриваете вариант неиспользования gzip?


          1. Balling
            08.09.2024 20:55

            Никто не рассматривает вариант неиспользования gzip, кроме вас. Это баг в ffmpeg. https://trac.ffmpeg.org/ticket/7158

            Youtube использует такой DASH для live стримов.


            1. mayorovp
              08.09.2024 20:55
              +9

              ...и как же этот баг в ffmpeg связан с причиной, по которой GitHub Pages не поддерживает brotli?


              1. Balling
                08.09.2024 20:55

                brotli не поддерживается nginx по умолчанию. Скорее всего это.


                1. mayorovp
                  08.09.2024 20:55
                  +8

                  Это понятно и скорее всего и есть настоящая причина. Но я всё ещё не понимаю логику ваших прошлых комментариев.


    1. SergeiMinaev
      08.09.2024 20:55
      +2

      Сервер будет сжимать только если клиент сам ему скажет "Accept-Encoding: gzip, br".


      1. redfox0
        08.09.2024 20:55

        gzip, deflate, br, zstd


    1. GennPen
      08.09.2024 20:55

      А зачем использовать неэффективный алгоритм, который ради прироста сжатия на 15% использует на порядок больше вычислительных ресурсов?


      1. Balling
        08.09.2024 20:55

        Потому что 90% времени процессор всё равно простаивает, дожидаясь данных через сеть.


        1. mrsantak
          08.09.2024 20:55

          Это утверждение далеко не всегда верно. У нас в проекте есть необходимость доставлять на виртуалки питонячий энв. По-сути, нужно скачать и распаковать на диск архивчик размером в пару гигов. Так вот, оказалось, что скачивать и распаковывать несжатый tar быстрее, чем скачивать и распаковывать сжатый tar.xz или tar.gz.


          1. mk2
            08.09.2024 20:55

            А если lz4?

            Для @GennPen- если ресурс статический, и можно 1 раз сжать и 1000 раз отдать контент, то потратить на порядок больше времени, получив -15% к размеру, становится выгоднее.


            1. mrsantak
              08.09.2024 20:55

              Мы там много всякого пробовали, и с параметрами конкретных алгоритмов игрались.В итоге оказалось, что овчинка выделки не стоит.


          1. Balling
            08.09.2024 20:55

            .xz был создан для исходного кода и бинарников типо elf, специально. Он всё ещё обеспечивает лучше сжатие через zstd, дефолтный архив apt-get. И у вас же не 14 Intel, и вообще xz не очень оптимизирован, но после скандала с бекдором его стали пилить как не в себя.

            Большие архивы это не веб страницы, web страницы надо ещё отрендерить, чем быстрее начнешь рендерить тем лучше.


            1. mrsantak
              08.09.2024 20:55
              +1

              Речь о том, что уменьшение времени скачивания в обмен на нагрузку cpu далеко не всегда даёт рост производительности. В вопросах оптимизации вообще с универсальными решениями туго.


              1. mayorovp
                08.09.2024 20:55

                Какой вообще "обмен на нагрузку cpu" в случае статических ресурсов?


                1. saboteur_kiev
                  08.09.2024 20:55
                  +2

                  так распаковать тоже надо?


                  1. mayorovp
                    08.09.2024 20:55

                    А разве на распаковку требуется много ресурсов?


                    1. saboteur_kiev
                      08.09.2024 20:55

                      у меня на умной розетке висит веб-сервер. Там каждые лишние 100 байт можно без секундомера ощутить.


                    1. mrsantak
                      08.09.2024 20:55

                      Я же про это и писал, у нас в проекте оказалось, что скачать и распаковать несжатый архив быстрее чем сжатый.


      1. Balling
        08.09.2024 20:55

        Вы раз минусуете, то я вас ещё рассказу. Вы знаете, что nvme и hdd такие медленные, что если сжать exe и dll то распаковка в оперативке займет меньше времени?


        1. GennPen
          08.09.2024 20:55
          +3

          Причем тут nvme и hdd, exe и dll?


        1. GennPen
          08.09.2024 20:55
          +3

          Объясняю, почему Brotli нельзя включать на проде.

          Заходим на официальный репозиторий, далее ищем в нем ссылку на большой тест алгоритмов сжатия.

          В нем ищем нужные результаты:

                          Compression                      Compressed size      Decompresser  Total size   Time (ns/byte)
          Program           Options                       enwik8      enwik9     size (zip)   enwik9+prog  Comp Decomp   Mem Alg Note
          -------           -------                     ----------  -----------  -----------  -----------  ----- -----   --- --- ----
          brotli 18-Feb-2016 -q 11 -w 24                25,764,698  223,597,884    542,385 s  224,140,269   3400   5.9  437 LZ77 48
          gzip 1.3.5        -9                          36,445,248  322,591,995     38,801 x  322,630,796    101    17  1.6 LZ77

          Сжатие XML-файла примерно на 30% эффективней. Но самое главное - это колонка "Time Comp" по которой видно, что brotli сжимается в 34(!) раза медленней и памяти требуется в 273(!) раза больше.

          Скрытый текст


          1. yrub
            08.09.2024 20:55
            +2

            Вы видимо плохо понимаете как читать результаты и какие вообще требования к алгоритмам сжатия сегодня. А сегодня требуется максимальная гибкость, чтобы использовать один алгоритм для всего, вместо вороха разных, в особенности эта концепция развита в zsd, когда на минимальном сжатии мы примерно по скорости как lz4, а на максимуме жмем рекордно как 7zip, и нам все равно сколько времени это займет.

            11 уровень у бротли это ультра, максимальное сжатие, но если вы попробуете другие уровни, то внезапно окажется что Brotli жмет процентов на 10% лучше чем gzip да и еще делает это заметно быстрее.

            А если мне надо 1 раз во время билда сжать css-js в пару мб, то я включу это самое ультра сжатие, заплачу за него целую секунду работы процессора, вместо миллисекунд для gzip, зато сэкономлю трафик и время пользователя.


            1. DmitryOlkhovoi
              08.09.2024 20:55

              Ну сжать билд или сжать ответ это разные вещи)


              1. yrub
                08.09.2024 20:55
                +2

                вы читать написанное умеете? ;) первый абзац, бротли это не gzip, у которого параметры сжатия +- в узких пределах. У бротли 12 уровней сжатия, 6-ой стандартный, его используйте для обычного ответа. Это будет быстрее по времени чем gzip (на макс) и лучше по уровню сжатия. Есть расширенная версия 7zip ZS, там и бротли и zstd, можно поэкспериментировать. Есть еще такое https://quixdb.github.io/squash-benchmark/


                1. DmitryOlkhovoi
                  08.09.2024 20:55

                  Ну бенчмарк это хорошо, только я по прежнему не понимаю как мне поможет бротли в динамике и как он ведёт себя с тонким клиентом. У меня вот есть гзип или даже нечего иногда. Все работает, ттб в норме. Бротли уже не мало лет, кроме его реализации браузерами, его распространение сомнительное, цена его использование - вычеслительная мощность в архитектуре. Зачем? Ради пары килобайт? Как это повлияет на конверсию?

                  Чисто вот по факту, есть допустим высконагруженный проект, в плане посетителей. Картинки, текст, и прочее. Сменю я гзип на бротли. Цена этого? В плате за сервер и ттб на клиенте


                  1. yrub
                    08.09.2024 20:55

                    его распространение сомнительное

                    все зависит от квалификации людей. вот facebookу надо - он вкладывается в zstd, а некоторые даже на http2 перейти не могут. Вопрос в другом, зачем нужен gzip если есть brotli который жмет лучше процентов на 10% и работает быстрее, причем все что надо от администратора это или поменять пару строчек в конфиге или выбрать другой пункт из дропдауна. цену я за вас определить не могу, для этого надо знать объем текстового трафика в сутки, но учитывая, что в большинстве случаев brotli сегодня включается элементарно и поддерживается уже всеми клиентами, я не понимаю в чем проблема его включить. в принципе новые алгоритмы и пакуют быстрее и распаковывают тоже, так что тонкому клиенту будет лучше. Глобально все оптимизации дают по 5-10%, если их рассматривать индивидуально, а интегрально набежит на порядок больше;) в chrome dev tools можно установить параметры как у сотовой сети, возможно получится заметить latency глазом


                    1. DmitryOlkhovoi
                      08.09.2024 20:55

                      все зависит от квалификации людей

                      ой да хватит))


                      1. yrub
                        08.09.2024 20:55

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


                      1. DmitryOlkhovoi
                        08.09.2024 20:55

                        ну чисто обжать бандл в билдтайм можно угу


                  1. saboteur_kiev
                    08.09.2024 20:55
                    +2

                    Как раз в браузерах он уже вроде везде поддерживается, это же гугл его продвигает.
                    В 2017 году он поддерживался уже всеми популярными браузерами, в том числе и curl, а также популярными серверами (apache/nginx)


                    1. redfox0
                      08.09.2024 20:55

                      Скомпилировал для nginx модуль https://github.com/google/ngx_brotli. Так динамическое сжатие уменьшило исходящий трафик где-то в три раза, на некоторых файлах экономия доходила до шести раз.

                      Можно было бы сжать статические файлы и отдавать их (nginx и такое умеет), но больше возни с настройками, повышение нагрузки на процессоры не замечено.


                      1. saboteur_kiev
                        08.09.2024 20:55

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


            1. GennPen
              08.09.2024 20:55
              +2

              А много ли статичного контента в интернете? Лично я его уже практически не вижу. Даже якобы статичный контент зачастую генерируется динамически. А всякие css/js прекрасно кэшируются на стороне клиента, что экономит кучу трафика.

              И еще на сколько помнится, gzip использует блочную компрессию, что позволяет начинать распаковывать валидные данные при частичном получении данных. Про brotli я подобной информации не нашел, возможно плохо искал.


              1. yrub
                08.09.2024 20:55

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

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


                1. saboteur_kiev
                  08.09.2024 20:55

                  что zstd умеет еще делать пред прогон всего файла перед сжатием и составлять по нему оптимальный словарь. но думаю и в этом случае сжатие идет блочно, хотя это и более сложный случай.

                  Это же противоречит блочности.
                  Каждый блок должен был независим от другого блока, чтобы иметь возможносьт сжать блок, передать и разжать на той стороне, пока начинает сжиматься и передаваться второй блок.


                  1. yrub
                    08.09.2024 20:55

                    надо смотреть как это технически устроено, я никогда не использовал. принципиально это не мешает сжимать и распаковывать блоки параллельно, после осмотра всего файла, в худшем случае они будут зависеть от первого блока или какого-то файла, чатгпт говорит что распаковка и в этом случае идет отдельными блоками и может осуществляться параллельно. но это опциональная фича, на сколько помню она заметно повышает степень сжатия. в принципе этот словарь можно использовать потом и для сжатия похожих файлов. ну т.е. вы потренировали zstd на своих реквестах-респонсах и после используете эти данные во время работы приложения. года 4 назад на хабре про это писали. но это все фичи для мобильного приложения, браузеры не умеют работать с zstd пока. В общем случае имеем такую же блочную манеру обработки как и в gzip.


                    1. saboteur_kiev
                      08.09.2024 20:55

                      Если блок зависит от другого блока, он не может распаковываться параллельно. Это как солид архив.

                      Как раз паралельно можно разжимать независимые блоки. Либо передавать тогда весь словарь до того, как начали разжимать блоки.

                      В бротли есть встроенный словарь популярных слов для web, что собственно и дает ему первичный прирост в 10-15% даже на сопоставимом с zip уровне сжатия на всяких xml/html/css. Там вроде около 120 кб.



  1. DmitryOlkhovoi
    08.09.2024 20:55
    +4

    Хорошо, вы уменьшили трафик, но что начет отображение контента? Насколько дольше он теперь отображается. У меня например оригинал статьи с лагом в несколько секунд дозагружается на вашем блоге)) 5 секунд по факту. Это настольный пк.
    И с заметным прыганьем. И как-то смешно считать эти килобайты, когда шрифт на странице весит 800кб+

    I want to provide a smooth experience to my site visitors

    Вообще не смузи) Еще и какой-то пустой скрол в несколько экранов

    Но все очень интересно, так держать)


    1. DmitryOlkhovoi
      08.09.2024 20:55
      +1

      A different comment says librewolf disables webgl by default, breaking OP's decompression.

      Нашел ответ) На маке лучше, но там все равно блинк контента, скрол и мы попрежнему, вместо - скачать страницу -> отрендерить получаем - скачать страницу -> скачать сжатый файлик -> анзип -> и только тут рендер. А если гидрация нужна, подождем еще.


  1. zoto_ff
    08.09.2024 20:55
    +10

    странная цель - делать сайт как можно меньше в ущерб его производительности.

    90 килобайт - сущие копейки. комментатор выше верно подметил: своей "оптимизацией" вы ускорили загрузку страницы на несколько миллисекунд, но взамен сильно замедлили её отображение


    1. Dadadam999
      08.09.2024 20:55
      +3

      Согласен полностью и хочу добавить, что бизнесу и пользователям важнее время отображения, а не загрузки данных. Загрузку данных в теории можно исправить на уровне железа и сети, а вот если всё упирается в алгоритмы отображения, то кроме как переписываем кода проблему не решить. Конечно сжатие важно, но его нужно применять с умом и где это действительно нужно, а не ради сохранения 90 килобайт во вред производительности.


  1. yrub
    08.09.2024 20:55
    +2

    Как правило, Brotli лучше, чем gzip, а gzip лучше, чем ничего. gzip настолько малозатратен, что он на серверах по умолчанию включен, а вот Brotli на порядок медленнее.

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


  1. zzzzzzzzzzzz
    08.09.2024 20:55
    +5

    Ждал в конце чего-то в духе: "на проде, конечно, мы так делать не будем".
    Но не дождался.
    Так что восприятие статьи перешло из жанра триллера в ужасы.


    1. sleirsgoevy
      08.09.2024 20:55
      +2

      Ну и блин, прикольно же. Мне нравится прикалываться!

      Не оно?


  1. pvvv
    08.09.2024 20:55
    +2

    декомпрессор xz (https://github.com/tukaani-project/xz-embedded) собранный wasm - 6373 байта.

    при том руками оптимизированный на асм для х86 там вообще в меньше кБ, так что есть куда расти.

    а жать он должен получше webp.


  1. aliencash
    08.09.2024 20:55

    А можно протестировать Avif и JpegXL?


  1. kovalensky
    08.09.2024 20:55

    Почему не привязали cloudflare, добавили заголовок из сжатия?