· 10 мин

Как мы победили «монстров» длинных URL: от Base64 к LZ-String

История о том, как мы решили проблему передачи крупных JSON-фильтров через URL, перепробовали несколько подходов и остановились на LZ-String

Максим Шек
Максим Шек
Генеральный директор

Введение

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

После JSON.stringify получался «монстр» из сотен символов, который мы толкали в URL. Казалось бы — не проблема, но как только в фильтр добавлялись ещё пара полей, ссылка гарантированно переваливала за 1000 символов.

И тогда мы:

  • написали ручной парсер на ?field[]=… и ?field[0][sub]=… — оказалось тяжело поддерживать;
  • переключились на Base64 — думали, что будет достаточно;
  • но поняли: это решение живёт недолго.

Откуда растут ноги у длинных URL

Передача параметров фильтрации часто выглядит просто:

Простой запрос
GET /items?category=electronics&price_min=100&price_max=500&tags=new,sale,popular
А вот так выглядит реальный запрос с фильтрами — 454 символа
GET /search/vacancy?text=Vue&search_field=name&search_field=company_name&search_field=description&excluded_text=%D0%A1%D0%BB%D0%BE%D0%B2%D0%BE&professional_role=4&professional_role=62&professional_role=70&industry=15.545&industry=15.546&industry=15.543&industry=15.542&industry=15.544&industry=15.548&industry=15.549&area=1&salary=&currency_code=RUR&experience=doesNotMatter&order_by=relevance&search_period=0&items_on_page=50

Когда фильтр усложняется

Но когда фильтр становится сложным — глубокие вложенности, сотни ID, логические группы вроде (A ИЛИ B) И (C ИЛИ (D И E)) — после JSON.stringify строка легко превышает 300–500 символов, а Base64 её раздувает до 600+.

Проблемы:

  • Обрезается в логах nginx/apache;
  • Не проходит через старые прокси;
  • Сложно читать и отлаживать.

Классические методы сериализации

Прежде чем найти решение, мы попробовали стандартные подходы.

Сравнение подходов

Query-параметры

Нет зависимостей, просто реализовать. Но плохо читается при множестве полей и не поддерживает вложенные объекты.

Base64-кодирование

Сохраняет структуру JSON, поддерживается везде. Но добавляет +33% к размеру — URL всё равно получается тяжёлым.

base64-approach.ts
const json = JSON.stringify(filters); // 454 символа
const b64  = btoa(json);              // 628 символов
fetch(`/items?filters=${encodeURIComponent(b64)}`);

Как мы наткнулись на LZ-String

LZ-String — это JS-библиотека на основе алгоритма LZ77 с безопасным для URL кодированием.

Почему она нас заинтересовала:

  • Компактность — удаляет повторяющиеся фрагменты;
  • Простота — лёгкий API compressToEncodedURIComponent;
  • Кроссплатформенность — порты для PHP, Python, C#, Java.

LZ77 ищет повторы в данных и заменяет их ссылками на предыдущие вхождения. LZ-String затем кодирует результат в URL-безопасную строку. Чем больше повторов — тем выше коэффициент сжатия.


Интеграция в проект

Внедрение оказалось максимально простым — и на фронте, и на бэке.

frontend.ts
import LZString from 'lz-string';

function buildFilterURL(filters: object): string {
  const json       = JSON.stringify(filters);
  const compressed = LZString.compressToEncodedURIComponent(json);
  return `/items?filters=${compressed}`;
}
backend.php
use LZCompressor\LZString;

$raw     = $_GET['filters'] ?? '';
$json    = LZString::decompressFromEncodedURIComponent($raw);
$filters = json_decode($json, true);
backend.ts
import LZString from 'lz-string';

const raw = req.query.filters || '';
const filters = JSON.parse(
  LZString.decompressFromEncodedURIComponent(raw)
);

Бенчмарки

Мы провели замеры на реальных данных проекта.

454
символов JSON
628
символов Base64
436
символов LZ-String

Скорость обработки (JSON ≈ 3 КБ)

Base64

Сжатие: 0.12 мс, распаковка: 0.10 мс

LZ-String

Сжатие: 0.40 мс, распаковка: 0.35 мс


Примеры «до» и «после»

На простых фильтрах выигрыш скромный, но на сложных — существенный.

Простой фильтр

JSON: 89 символов → Base64: 120 символов → LZ-String: 82 символа

Сложный вложенный фильтр

JSON: 312 символов → Base64: 416 символов → LZ-String: 276 символов


Подводные камни

  • Зависимость: +≈10 КБ к бандлу;
  • CPU: при сотнях запросов/сек может быть заметна нагрузка на сжатие;
  • Кодировки: всегда используйте compressToEncodedURIComponent + обратный метод;
  • Безопасность: валидируйте вход на бэке, чтобы избежать zip-бомб.

Заключение

Мы прошли путь от ручного парсинга до Base64, затем открыли LZ-String, провели замеры, внедрили на фронте и бэке, написали тесты и отладили фоллбэк.

В итоге:

  • Ссылки короче — ≈30% экономии vs Base64;
  • Чистый код — единый метод сериализации;
  • Надёжность — валидация и fallback на все случаи.
Нужна помощь с оптимизацией?

Разберём вашу задачу и предложим решение

Обсудить проект

Другие статьи