Я хочу, чтобы посетители моего сайта наслаждались им, так что я забочусь об 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 кодирует разные каналы разными деревьями Хаффмана, поэтому на однотонные каналы тратится места.
$ 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 с содержимым этой страницы:
На самом деле в коде используется высокое и узкое изображение, но на эту картинку смотреть приятнее.
Светлая часть вверху и в самом низу — текст и код. Полосатая часть на 20% по высоте — диаграмма. Тёмная часть, занимающая больше всего места — текст на диаграмме (да, там не используется шрифт).
Несколько ярких пикселей среди текста? Это символы Юникода, в основном знаки препинания вроде апострофа и троеточия.
На самом деле, сэкономили мы тут только на спичках: исходная версия, сжатая через gzip, занимает 88 килобайт, а версия, сжатая через WebP и gzip — 83 килобайта. При этом Brotli выдал бы 69 килобайт. Всё лучше чем ничего.
Ну и блин, прикольно же. Мне нравится прикалываться!
Ссылки
Код на Rust, корпус и некоторые другие файлы доступны на GitHub.
Если хотите, можете присоединиться к обсуждению на Reddit или на Hacker News.
Комментарии (52)
DmitryOlkhovoi
08.09.2024 20:55+4Хорошо, вы уменьшили трафик, но что начет отображение контента? Насколько дольше он теперь отображается. У меня например оригинал статьи с лагом в несколько секунд дозагружается на вашем блоге)) 5 секунд по факту. Это настольный пк.
И с заметным прыганьем. И как-то смешно считать эти килобайты, когда шрифт на странице весит 800кб+I want to provide a smooth experience to my site visitors
Вообще не смузи) Еще и какой-то пустой скрол в несколько экранов
Но все очень интересно, так держать)
DmitryOlkhovoi
08.09.2024 20:55+1A different comment says librewolf disables webgl by default, breaking OP's decompression.
Нашел ответ) На маке лучше, но там все равно блинк контента, скрол и мы попрежнему, вместо - скачать страницу -> отрендерить получаем - скачать страницу -> скачать сжатый файлик -> анзип -> и только тут рендер. А если гидрация нужна, подождем еще.
zoto_ff
08.09.2024 20:55+10странная цель - делать сайт как можно меньше в ущерб его производительности.
90 килобайт - сущие копейки. комментатор выше верно подметил: своей "оптимизацией" вы ускорили загрузку страницы на несколько миллисекунд, но взамен сильно замедлили её отображение
Dadadam999
08.09.2024 20:55+3Согласен полностью и хочу добавить, что бизнесу и пользователям важнее время отображения, а не загрузки данных. Загрузку данных в теории можно исправить на уровне железа и сети, а вот если всё упирается в алгоритмы отображения, то кроме как переписываем кода проблему не решить. Конечно сжатие важно, но его нужно применять с умом и где это действительно нужно, а не ради сохранения 90 килобайт во вред производительности.
yrub
08.09.2024 20:55+2Как правило, Brotli лучше, чем gzip, а gzip лучше, чем ничего. gzip настолько малозатратен, что он на серверах по умолчанию включен, а вот Brotli на порядок медленнее.
Начем с того, что это не правда. Brotli всегда лучше и всегда быстрее чем gzip. Просто надо использовать адекватный уровень сжатия.
zzzzzzzzzzzz
08.09.2024 20:55+5Ждал в конце чего-то в духе: "на проде, конечно, мы так делать не будем".
Но не дождался.
Так что восприятие статьи перешло из жанра триллера в ужасы.
pvvv
08.09.2024 20:55+2декомпрессор xz (https://github.com/tukaani-project/xz-embedded) собранный wasm - 6373 байта.
при том руками оптимизированный на асм для х86 там вообще в меньше кБ, так что есть куда расти.
а жать он должен получше webp.
saboteur_kiev
Вы же сами ответили на свой вопрос.
Гитхаб это не обычный веб-сайт, это сервис, в котором исходники часто отдают по https а не ssh, и гитхаб могут использовать для различных активностей, автоматизации, даже синхронизации конфигов с различных устройств, в том числе и смарт-устройств, работающих на 1-5 ватт, где процессор может быть запитан от маломощной батарейки.
Дать пользователям выбор наверное было бы лучшим вариантом, но видимо может быть софт до 2016-2017 года, который автоматом это не подхватит. Не знаю, я честно говоря не могу сказать насколько легко оставить обратную совместимость.
purplesyringa
Речь о GitHub Pages, который для исходников, конфигов и прочего (обычно) не используется.
HTTP-клиент передает допустимые форматы в загаловке Accept-Encoding, сервер далее имеет право сжимать только каким-либо из этих форматов. Проблем с совместимостью благодаря этому быть не должно.
Balling
Это вообще бред, что вы несете. Да если бы youtube приложение/chrome не использовало gzip там было бы 6 мбайт данных (не мбит) в секунду на одном тексте... Так и есть в ffmpeg/mpv, так как там gzip и br не поддерживается. https://github.com/mpv-player/mpv/issues/14360
После сжатия 300 кбайт.
mayorovp
Не вижу в вашем комментарии логики. Откуда вообще взялся Ютуб и почему вы вообще рассматриваете вариант неиспользования gzip?
Balling
Никто не рассматривает вариант неиспользования gzip, кроме вас. Это баг в ffmpeg. https://trac.ffmpeg.org/ticket/7158
Youtube использует такой DASH для live стримов.
mayorovp
...и как же этот баг в ffmpeg связан с причиной, по которой GitHub Pages не поддерживает brotli?
Balling
brotli не поддерживается nginx по умолчанию. Скорее всего это.
mayorovp
Это понятно и скорее всего и есть настоящая причина. Но я всё ещё не понимаю логику ваших прошлых комментариев.
SergeiMinaev
Сервер будет сжимать только если клиент сам ему скажет "Accept-Encoding: gzip, br".
redfox0
gzip, deflate, br, zstd
GennPen
А зачем использовать неэффективный алгоритм, который ради прироста сжатия на 15% использует на порядок больше вычислительных ресурсов?
Balling
Потому что 90% времени процессор всё равно простаивает, дожидаясь данных через сеть.
mrsantak
Это утверждение далеко не всегда верно. У нас в проекте есть необходимость доставлять на виртуалки питонячий энв. По-сути, нужно скачать и распаковать на диск архивчик размером в пару гигов. Так вот, оказалось, что скачивать и распаковывать несжатый tar быстрее, чем скачивать и распаковывать сжатый tar.xz или tar.gz.
mk2
А если lz4?
Для @GennPen- если ресурс статический, и можно 1 раз сжать и 1000 раз отдать контент, то потратить на порядок больше времени, получив -15% к размеру, становится выгоднее.
mrsantak
Мы там много всякого пробовали, и с параметрами конкретных алгоритмов игрались.В итоге оказалось, что овчинка выделки не стоит.
Balling
.xz был создан для исходного кода и бинарников типо elf, специально. Он всё ещё обеспечивает лучше сжатие через zstd, дефолтный архив apt-get. И у вас же не 14 Intel, и вообще xz не очень оптимизирован, но после скандала с бекдором его стали пилить как не в себя.
Большие архивы это не веб страницы, web страницы надо ещё отрендерить, чем быстрее начнешь рендерить тем лучше.
mrsantak
Речь о том, что уменьшение времени скачивания в обмен на нагрузку cpu далеко не всегда даёт рост производительности. В вопросах оптимизации вообще с универсальными решениями туго.
mayorovp
Какой вообще "обмен на нагрузку cpu" в случае статических ресурсов?
saboteur_kiev
так распаковать тоже надо?
mayorovp
А разве на распаковку требуется много ресурсов?
saboteur_kiev
у меня на умной розетке висит веб-сервер. Там каждые лишние 100 байт можно без секундомера ощутить.
mrsantak
Я же про это и писал, у нас в проекте оказалось, что скачать и распаковать несжатый архив быстрее чем сжатый.
Balling
Вы раз минусуете, то я вас ещё рассказу. Вы знаете, что nvme и hdd такие медленные, что если сжать exe и dll то распаковка в оперативке займет меньше времени?
GennPen
Причем тут nvme и hdd, exe и dll?
GennPen
Объясняю, почему Brotli нельзя включать на проде.
Заходим на официальный репозиторий, далее ищем в нем ссылку на большой тест алгоритмов сжатия.
В нем ищем нужные результаты:
Сжатие XML-файла примерно на 30% эффективней. Но самое главное - это колонка "Time Comp" по которой видно, что brotli сжимается в 34(!) раза медленней и памяти требуется в 273(!) раза больше.
Скрытый текст
yrub
Вы видимо плохо понимаете как читать результаты и какие вообще требования к алгоритмам сжатия сегодня. А сегодня требуется максимальная гибкость, чтобы использовать один алгоритм для всего, вместо вороха разных, в особенности эта концепция развита в zsd, когда на минимальном сжатии мы примерно по скорости как lz4, а на максимуме жмем рекордно как 7zip, и нам все равно сколько времени это займет.
11 уровень у бротли это ультра, максимальное сжатие, но если вы попробуете другие уровни, то внезапно окажется что Brotli жмет процентов на 10% лучше чем gzip да и еще делает это заметно быстрее.
А если мне надо 1 раз во время билда сжать css-js в пару мб, то я включу это самое ультра сжатие, заплачу за него целую секунду работы процессора, вместо миллисекунд для gzip, зато сэкономлю трафик и время пользователя.
DmitryOlkhovoi
Ну сжать билд или сжать ответ это разные вещи)
yrub
вы читать написанное умеете? ;) первый абзац, бротли это не gzip, у которого параметры сжатия +- в узких пределах. У бротли 12 уровней сжатия, 6-ой стандартный, его используйте для обычного ответа. Это будет быстрее по времени чем gzip (на макс) и лучше по уровню сжатия. Есть расширенная версия 7zip ZS, там и бротли и zstd, можно поэкспериментировать. Есть еще такое https://quixdb.github.io/squash-benchmark/
DmitryOlkhovoi
Ну бенчмарк это хорошо, только я по прежнему не понимаю как мне поможет бротли в динамике и как он ведёт себя с тонким клиентом. У меня вот есть гзип или даже нечего иногда. Все работает, ттб в норме. Бротли уже не мало лет, кроме его реализации браузерами, его распространение сомнительное, цена его использование - вычеслительная мощность в архитектуре. Зачем? Ради пары килобайт? Как это повлияет на конверсию?
Чисто вот по факту, есть допустим высконагруженный проект, в плане посетителей. Картинки, текст, и прочее. Сменю я гзип на бротли. Цена этого? В плате за сервер и ттб на клиенте
yrub
все зависит от квалификации людей. вот facebookу надо - он вкладывается в zstd, а некоторые даже на http2 перейти не могут. Вопрос в другом, зачем нужен gzip если есть brotli который жмет лучше процентов на 10% и работает быстрее, причем все что надо от администратора это или поменять пару строчек в конфиге или выбрать другой пункт из дропдауна. цену я за вас определить не могу, для этого надо знать объем текстового трафика в сутки, но учитывая, что в большинстве случаев brotli сегодня включается элементарно и поддерживается уже всеми клиентами, я не понимаю в чем проблема его включить. в принципе новые алгоритмы и пакуют быстрее и распаковывают тоже, так что тонкому клиенту будет лучше. Глобально все оптимизации дают по 5-10%, если их рассматривать индивидуально, а интегрально набежит на порядок больше;) в chrome dev tools можно установить параметры как у сотовой сети, возможно получится заметить latency глазом
DmitryOlkhovoi
ой да хватит))
yrub
ну дело ваше, страшно включать или лень, не включайте, мне с этого ничего не перепадает) я просто к тому, что нет рациональных причин его не использовать, если с вашей точки зрения рационально использовать gzip. Единственный случай когда я бы не использовал сжатие, это если с сервера приходят маленькие респоны
DmitryOlkhovoi
ну чисто обжать бандл в билдтайм можно угу
saboteur_kiev
Как раз в браузерах он уже вроде везде поддерживается, это же гугл его продвигает.
В 2017 году он поддерживался уже всеми популярными браузерами, в том числе и curl, а также популярными серверами (apache/nginx)
redfox0
Скомпилировал для nginx модуль https://github.com/google/ngx_brotli. Так динамическое сжатие уменьшило исходящий трафик где-то в три раза, на некоторых файлах экономия доходила до шести раз.
Можно было бы сжать статические файлы и отдавать их (nginx и такое умеет), но больше возни с настройками, повышение нагрузки на процессоры не замечено.
saboteur_kiev
ну так прогресс же.
Многие вещи могут сдерживаться по разным причинам. Просто руки не дошли, нет времени и ресурсов на апгрейд инфраструктуры, ибо просто включить опцию это не просто так, а в крупной компании нужно обосновать, проанализировать, протестировать, обосновать не с точки зрения производительности, а с точки зрения выгоды компании.
Например если есть два провайдера, один включил сжатие, другой не включил. А в конце месяца взаиморасчеты по трафику, и тут то бротли оказывается плохой
GennPen
А много ли статичного контента в интернете? Лично я его уже практически не вижу. Даже якобы статичный контент зачастую генерируется динамически. А всякие css/js прекрасно кэшируются на стороне клиента, что экономит кучу трафика.
И еще на сколько помнится, gzip использует блочную компрессию, что позволяет начинать распаковывать валидные данные при частичном получении данных. Про brotli я подобной информации не нашел, возможно плохо искал.
yrub
они все блочные, что бротли что zstd, на максимальном уровне сжатия размер блока увеличивается вдвое от стандартного, других нюансов не помню. единственно, что zstd умеет еще делать пред прогон всего файла перед сжатием и составлять по нему оптимальный словарь. но думаю и в этом случае сжатие идет блочно, хотя это и более сложный случай.
про статический контент было как пример, когда сжатие нужно максимальное. оно пригодится когда пользователь открывает сайт в первый раз (и с мобилки), для бизнеса это тоже критичная метрика. в других случаях ставьте среднее, все равно будет лучше gzip.
saboteur_kiev
Это же противоречит блочности.
Каждый блок должен был независим от другого блока, чтобы иметь возможносьт сжать блок, передать и разжать на той стороне, пока начинает сжиматься и передаваться второй блок.
yrub
надо смотреть как это технически устроено, я никогда не использовал. принципиально это не мешает сжимать и распаковывать блоки параллельно, после осмотра всего файла, в худшем случае они будут зависеть от первого блока или какого-то файла, чатгпт говорит что распаковка и в этом случае идет отдельными блоками и может осуществляться параллельно. но это опциональная фича, на сколько помню она заметно повышает степень сжатия. в принципе этот словарь можно использовать потом и для сжатия похожих файлов. ну т.е. вы потренировали zstd на своих реквестах-респонсах и после используете эти данные во время работы приложения. года 4 назад на хабре про это писали. но это все фичи для мобильного приложения, браузеры не умеют работать с zstd пока. В общем случае имеем такую же блочную манеру обработки как и в gzip.
saboteur_kiev
Если блок зависит от другого блока, он не может распаковываться параллельно. Это как солид архив.
Как раз паралельно можно разжимать независимые блоки. Либо передавать тогда весь словарь до того, как начали разжимать блоки.
В бротли есть встроенный словарь популярных слов для web, что собственно и дает ему первичный прирост в 10-15% даже на сопоставимом с zip уровне сжатия на всяких xml/html/css. Там вроде около 120 кб.
event1
Потому что ускорение загрузки сайта на 2 секунды может увеличить конверсию на 30%