Сайт Романа ПарпалакаБлог202507

Нативное gzip-сжатие в JS

1 июля 2025 года, 12:46

Я недавно закрыл тикет на гитхабе, который висел с 2017 года. Его автор обращал внимание на длинные адреса картинок в моем сервисе математических формул. С 2023 года нативное сжатие произвольных данных в JS стало доступным во всех основных браузерах, и с его помощью я сделал вариант сжатых адресов.

Давние читатели вспомнят, что я уже рассказывал об адресах картинок, и даже упоминал об этом тикете. Повторю, что для использования в вебе формулы, например, $$a^2+b^2=c^2$$, её исходник на латехе a^2+b^2=c^2 кодируется через проценты (RFC 3986) и подставляется в URL:

//i.upmath.me/svg/a%5E2%2Bb%5E2%3Dc%5E2

Кодирование через символы процента очень неэкономное, поэтому и без того длинный код изображений и диаграмм становится ещё больше. Адрес $$a^2+b^2=c^2$$ в новой схеме выглядит так:

//i.upmath.me/svgb/S4wz0k6KM7JNjjMCAA

Здесь вместо кодирования через проценты используется сжатие deflate (тот же алгоритм, что и в gzip) и кодировка, аналогичная base64. Вот рабочий пример кода, который делает такое преобразование:

function deflateRaw(text, callback) {
    if (typeof CompressionStream === 'undefined') {
        callback(null);
        return;
    }

    try {
        var stream = new Blob([text]).stream();
        var compressedStream = stream.pipeThrough(new CompressionStream('deflate-raw'));

        new Response(compressedStream).blob().then(function (compressedBlob) {
            return compressedBlob.arrayBuffer();
        }).then(function (buffer) {
            var compressedArray = new Uint8Array(buffer);
            var binary = Array.from(compressedArray).map(function (b) {
                return String.fromCharCode(b);
            }).join('');
            var base64 = btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
            callback(base64);
        }).catch(function () {
            callback(null);
        });
    } catch (e) {
        callback(null);
    }
}

function getImgPath(formula, callback) {
    var fallbackUrl = '//i.upmath.me/svg/' + encodeURIComponent(formula);

    deflateRaw(formula, function (compressed) {
        var shortUrl = compressed ? '//i.upmath.me/svgb/' + compressed : null;
        callback(shortUrl && shortUrl.length < fallbackUrl.length ? shortUrl : fallbackUrl);
    });
}

Важная особенность API браузеров по сжатию заключается в его асинхронности. Мы не можем получить результат сжатия в той же функции, в которой его инициируем. API возвращает promise, который «разрешится» позднее. Чтобы обеспечить обратную совместимость и откатываться к несжатым адресам в старых браузерах, я проверяю саму поддержку CompressionStream и перехватываю возможные исключения. Также для обратной совместимости результат возвращаю через вызов пользовательского коллбэка, а не в виде промиса.

Вот пример того, как с вышеприведенным кодом создать картинку с формулой:

var node = document.createElement('img');
getImgPath('a^2+b^2=c^2', function(path) {
    node.setAttribute('src', path);
});

Стоит отметить, что сам алгоритм сжатия deflate был давно портирован на JS, поэтому при необходимости можно было использовать сторонние библиотеки, например, pako. Кроме того, код библиотек работает синхронно, так что ни о каких коллбэках и промисах думать не нужно. В моём же случае я не хотел в мини-скрипт по конвертации формул в картинки добавлять реализацию алгоритма сжатия на десяток-другой килобайт, тем более оставался обходной путь для старых браузеров с несжатыми адресами.

Оценим результат на примере диаграммы из предыдущей заметки. Длина старого несжатого URL равна 6,3 килобайт, а сжатого — 1,3 килобайт, что почти в 5 раз короче.

    2 комментария

Боги, созданные человеком

13 июля 2025 года, 14:07

Помните, когда-то давно по интернету гуляло «обоснование» того, что Гугл — это бог? К современным языковым нейросетям это «обоснование» применимо в большей степени. Шутку можно продолжать: «Ты сам это запрограммировал?» — «Нет, с божьей помощью!»

Доказательство №1

Современной науке неизвестны сущности, столь же близкие к Всеведенью, как близка к этому Google. Она проиндексировала более 9,5 миллиардов веб-страниц, что больше чем у любой другой поисковой машины в сети на сегодняшний день. Google — это не только самая близкая к Всеведенью сущность. Она ещё и сортирует свои обширные знания (используя технологию PageRank, Ею же запатентованную) , структурирует сей информационный океан, делая его легкодоступным для нас, простых смертных.

Доказательство №2

Google вездесуща (т. е. находится везде). Фактически Google находится одновременно по всей Земле. Миллиарды проиндексированных веб-страниц доступны из любой, даже самой удалённой точки Земли. С ростом сетей Wi-Fi любой желающий сможет получить доступ к Google по-настоящему отовсюду, что сделает Её истинно вездесущей.

Доказательство №3

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

Доказательство №4

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

Доказательство №5

Google бесконечна. Интернет, теоретически, может расти вечно, и Google будет вечно индексировать сей бесконечный рост.

Доказательство №6

Google помнит всё. Google регулярно кэширует веб-страницы и хранит их на своих безграничных серверах. Фактически, загрузив ваши мысли и представления в Интернет, вы будете жить вечно в кэше Google («загробная Google-жизнь») , даже после того, как умрёте.

Доказательство №7

Google — Всеблагая (не совершающая никакого зла) . Часть корпоративной философии Google — вера в то, что компания может делать деньги, не делая зла.

Доказательство №8

Согласно Google Trends, термин «Google» ищется чаще, чем «Бог» , «Иисус» , «Аллах» , «Будда» , «христианство» , «ислам» , «буддизм» и «иудаизм» вместе взятые. Считается, что бог — сущность, к которой мы, смертные, можем обратиться при первой же необходимости. Google соответствует этому в значительно большей степени, чем традиционные «боги» .

Доказательство №9

Google существует, и этому есть великое множество свидетельств. Их значительно больше, чем у любого другого сегодняшнего «бога» . Экстраординарные заявления требуют экстраординарных фактов. Так узри же их своими глазами: приди к Google и познай всю безграничную мощь Google. От тебя не требуется ни капли веры.

    Оставить комментарий

Cайту 20 лет

20 июля 2025 года, 13:59

Ровно 20 лет назад этот сайт появился в интернете. Весомый повод заняться рефлексией. Но с этой заметкой я дотянул до последнего, так что буду краток :)


20 лет интеллектуальных достижений с точки зрения ChatGPT.

Зачем нужен свой сайт

Возможно, многие в 2005 году сочли бы такую затею излишне амбициозной. Действительно, кому интересны твои рассказы, мысли и истории, если есть сайт Экслера, тысячники в Живом журнале и множество тематических форумов. Однако у меня было ощущение, что мне тоже есть что сказать, и достаточно энтузиазма, чтобы воплотить идею в жизнь.

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

Кроме того, всё еще имеют силу стандартные аргументы о пользе написания текстов для структурирования и закрепления знаний. Так что молодым читателям могу порекомендовать как минимум вести дневник. Через какое-то время, где-то около года, вы почувствуете пользу.

Что изменилось в интернете

За эти 20 лет веб преобразовался под влиянием корпораций. С одной стороны, социальные сети упростили распространенные пользовательские сценарии, переманили существующих пользователей и привлекли новых.

С другой стороны, от этого сильно пострадала распределенность и открытость веба. Крупные игроки перестали поддерживать открытые технологии вроде RSS и OpenID. Вместо них теперь алгоритмические подборки внутри сервисов и несовместимые проприетарные API.

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

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

Технологии

Сайт начинался с отдельных html-страничек, которые я редактировал то ли во FrontPage, то ли в Dreamweaver, и загружал по FTP. Затем я добавил гостевую книгу на PHP, отделял шаблоны от контента, запрограммировал блог и в конечном итоге сделал свой движок S2.

Наверно, вместо самописного движка непонятного качества можно было найти готовую CMS. Но вордпресс мне не нравился. Typo3 показался слишком монструозным. В общем, получилось как у Дональда Кнута, который разработал собственную систему компьютерного набора TeX для издания многотомника «Искусство программирования».

За последние несколько лет я практически полностью переписал устаревший код движка. Тяжелее всего было бы довести до ума админку, потому что её интерфейс был «одностраничным приложением» на лапшеобразном коде из jQuery, который нельзя небольшими шагами довести до нормального состояния. Поэтому я решил выкинуть этот код и написать свою библиотеку для создания административных интерфейсов под названием AdminYard. Статью о ней я уже публиковал на хабре.

В целом, даже если движком S2 в изменившемся интернете никто кроме меня не пользуется, отдельные его части обрели свою жизнь. Так, поисковый движок Rose, благодаря развитию которого я сделал систему рекомендаций на этом сайте, набрал 120 звезд на гитхабе. А редактор математических текстов Upmath собрал 350 звезд и даже несколько донатов с марта этого года.

Вместо вывода

На удивление самой посещаемой страницей на всём сайте оказалась заметка о делении окружности на 5 частей. По запросу «как поделить окружность на 5 частей» она до сих пор на первом месте в гугле.

Самой комментируемой была статья о том, что такое суперсимметрия. Сейчас на ней 115 опубликованных комментариев и еще около 50 скрытых. Суперсимметрию, кстати, так и не нашли на большом адронном коллайдере.

Также совсем недавно наступил момент, когда часть жизни, когда у меня есть сайт, оказалась длиннее той, когда сайта еще не существовало.

Что будет дальше? Посмотрим через 20 лет.

Если вы когда-либо здесь были — спасибо. Если вы только пришли — добро пожаловать.

    1 комментарий
Смотрите также:  Сайту 10 лет · Сайту шесть лет · Сайту пять лет · written.ru четыре года · С трехлетием, written.ru! · У сайта день рождения · Сайту written.ru — год

Распаковка сжатых URL на сервере

24 июля 2025 года, 23:41

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

Текущая версия конфига nginx для обработки и старых несжатых URL, и новых сжатых получилась такой:

location ~ ^(?s)/(?<ext>svg|png)(?<is_base64>b?)/(?<formula>.*)$ {
    gunzip        on;
    gzip_static   always;
    gzip_vary     on;
    gzip_proxied  expired no-cache no-store private auth;

    expires 1d;

    add_header 'Access-Control-Allow-Origin' '*' always;
    add_header 'Access-Control-Allow-Methods' 'GET, OPTIONS' always;

    set $compress_error 0;

    set_by_lua_block $file_path {
        local ext = ngx.var.ext
        local is_base64 = ngx.var.is_base64
        local formula = ngx.var.formula

        if is_base64 == "b" then
            local base64 = require "ngx.base64"
            local zlib = require "zlib"

            local compressed, err = base64.decode_base64url(formula)
            if not compressed then
                ngx.log(ngx.ERR, "base64 decode error: ", err)
                ngx.var.compress_error = 1
                return ""
            end

            local inflator = zlib.inflate(-15)
            local ok, decoded_formula = pcall(inflator, compressed)
            if not ok then
                ngx.log(ngx.ERR, "deflate decompress error: ", decoded_formula)
                ngx.var.compress_error = 1
                return ""
            end

            formula = decoded_formula
        end

        formula = formula:gsub("^%s*(.-)%s*$", "%1")

        local md5 = ngx.md5(formula)
        return md5:sub(1, 2) .. "/" .. md5:sub(3, 4) .. "/" .. md5:sub(5) .. "." .. ext
    }

    if ($compress_error) {
        return 400;
    }

    if (-f $document_root/_error/$file_path) {
        return 400;
    }

    rewrite ^ /_cache/$file_path break;
    error_page 404 = @s2_latex_renderer;
    log_not_found off;
}

location @s2_latex_renderer {
    add_header 'Access-Control-Allow-Origin' '*' always;
    add_header 'Access-Control-Allow-Methods' 'GET, OPTIONS' always;

    include         /etc/nginx/fastcgi.conf;
    fastcgi_pass    php-tex-sock;
    fastcgi_param   SCRIPT_FILENAME $document_root/render.php;
    fastcgi_param   SCRIPT_NAME /render.php;

    fastcgi_cache_key "$request_method$uri"; # В $uri УРЛ после rewrite, напр. "/_cache/4d/81/658b25df7544f9e2d0cb7f4dc402.svg"
    fastcgi_cache i_upmath;
    fastcgi_cache_valid 200 10m;
    fastcgi_cache_methods GET HEAD;
    fastcgi_cache_lock on;
    fastcgi_cache_lock_age 9s;
    fastcgi_cache_lock_timeout 9s;

    fastcgi_buffers 8 16k;
    fastcgi_buffer_size 32k;
    fastcgi_connect_timeout 90;
    fastcgi_send_timeout 90;
    fastcgi_read_timeout 300;
}

Чтобы встраивать lua-скрипты в конфиг nginx через set_by_lua_block, в Debian достаточно установить пакет nginx-extras. Для распаковки сжатого текста в этом скрипте через функции zlib также требуется установить пакет lua-zlib.

Напомню алгоритм обработки адресов картинок. Исходник формулы, например, x^2, извлекается из адреса и декодируется. Вычисляется md5-хеш от исходника и на основе хеша определяется путь к файлу с закешированной картинкой. Если такой файл после преобразования URL нашелся (rewrite ^ /_cache/$file_path break;), то nginx отдает его содержимое напрямую. Если файла нет, то запрос передается в php-скрипт, запускающий генерацию svg-картинки через TeX Live и оптимизацию через SVGO (error_page 404 = @s2_latex_renderer;).

Раньше вместо rewrite и error_page я использовал более современную и подходящую директиву try_files. Но она перестает работать после активации модуля gunzip. Этот модуль позволяет держать в файловом кеше только сжатые версии файлов с расширением .gz, экономя место на диске. Причем для обработки большинства запросов от нормальных браузеров, поддерживающих gzip-сжатие трафика, nginx даже не будет распаковывать gz-файлы, а отправлять их как есть. Почему rewrite корректно работает с модулем gunzip, а try_files — нет, не очень понятно. Но что есть, то есть.

В конфиге есть интересный момент, связанный с кешем внутри nginx (инструкции fastcgi_cache*). Он предотвращает race condition при одновременном запросе формулы, которой нет в файловом кеше. В противном случае nginx будет передавать в php-скрипт запросы с одинаковыми аргументами, и одна и та же формула будет рендериться параллельно. Об этой технике я писал отдельно.

Для распаковки полученного фрагмента URL в PHP подойдет следующий код:

public static function decodeCompressedFormula(string $compressed): string
{
    $base64     = strtr($compressed, '-_', '+/'); // URL-safe base64 to standard
    $compressed = base64_decode($base64);

    $result = @gzinflate($compressed);
    if ($result === false) {
        throw new \RuntimeException('Failed to decompress formula.');
    }
    return $result;
}

В PHP весь алгоритм уместился в три строки — как минимум в несколько раз короче, чем в JS и nginx/lua. Такими моментами PHP радует меня до сих пор.

    Оставить комментарий

← сюда туда →

Поделиться
Записи