Порой пользователю сайта нужно поделиться не просто ссылкой на страницу, но и её текущим состоянием: настройкой фильтров, содержимым редактора и так далее. Обычно для этого достаточно query-параметров, но по мере увеличения объёма данных приходится отдельно думать об их кодировании и сжатии.
Я столкнулся с такой проблемой при создании визуального редактора JSON Schema Schemagic, в котором пользователь может быстро создать схему и поделиться ею. На сайте нет регистрации, а схемами можно делиться через URL, как в TypeScript Playground и похожих сервисах.
Когда состояние стоит хранить в URL
Прежде чем решать, как кодировать и сжимать данные, стоит проверить, подходит ли URL для их хранения.
После сжатия состояние перестаёт быть читаемым. Это нормально, когда по ссылке передают большой фрагмент данных, который всё равно неудобно редактировать вручную. Но состояние с небольшим числом параметров лучше оставить в query-параметрах. URL вида ?type=article легко читается и при необходимости правится вручную прямо в адресной строке.
Хранение данных в URL накладывает свои ограничения: нельзя организовать совместное редактирование документа, настроить уровни доступа или отозвать уже отправленные данные. Если это необходимо, придётся хранить состояние на бэкенде.
Где хранить данные в URL
Данные можно разместить в нескольких частях URL: пути, query-параметрах или фрагменте.
Обычно путь определяет, какую страницу нужно открыть, а не текущее состояние веб-приложения.
Query-параметры хорошо подходят, когда нужно сохранить небольшой объём данных, например выбранные фильтры, сортировку или активный редактор. Но при переходе по такому URL браузер отправляет query-параметры серверу. В Schemagic схема может быть большой и содержать приватные данные, поэтому её нельзя отправлять на бэкенд.
Для хранения лучше всего подходит фрагмент (часть адреса после #). Браузер не отправляет фрагмент вместе с HTTP-запросом: состояние остаётся на клиенте. Для такого сценария это наиболее естественное место. Например, в TypeScript Playground строка с кодом хранится во фрагменте: #code/MYewdgzgLgBArgJwDYwLwwEQAspQA4QBcA9MQJYBuAhgNZlgB0AJgKYXEYDcQA.
Сериализация и кодирование
Наивное решение — сериализовать объект в JSON и записать получившуюся строку прямо в URL:
const dataStr = JSON.stringify(dataObj); const url = `${BASE_URL}#${dataStr}`;
У такого подхода две проблемы: некоторые символы JSON приходится экранировать, а сама строка может оказаться слишком длинной. Сжатие решает вторую проблему, но производит бинарные данные, которые нельзя напрямую записать в URL. Сначала их нужно преобразовать в безопасный для URL текст. Библиотека js-base64 позволяет закодировать данные в Base64url — вариант Base64 с безопасным для URL алфавитом.
В итоге весь конвейер выглядит следующим образом: состояние → сериализация → сжатие → Base64url. При чтении данных из URL операции выполняются в обратном порядке.
История браузера
Если поэкспериментировать с TypeScript Playground, можно заметить, что URL меняется только после потери фокуса, а переходы по истории браузера не возвращают предыдущие состояния кода. Если состояние часто меняется, постоянное сжатие может заметно замедлять работу интерфейса. Поэтому URL лучше обновлять не при каждой правке, а после небольшой паузы.
Также не стоит создавать новую запись в истории браузера при каждой правке. Кнопка «Назад» должна уводить со страницы, а не отменять изменения по одному. Обновлять URL без новой записи в истории и без перезагрузки страницы можно с помощью history.replaceState().
Ограничения длины URL
Единого ограничения длины URL нет: оно зависит от браузера и сервисов, через которые ссылкой делятся. Например, почтовый клиент или мессенджер может обрезать длинную ссылку. Поэтому следует задавать подходящий для проекта предел исходя из максимального объёма данных, который должно поддерживать приложение.
Сжимаем данные
При выборе способа сжатия важны коэффициент сжатия, скорость сжатия и распаковки, размер клиентского бандла и поддержка нужных браузеров. Я сравнил lz-string, pako, fflate и нативный CompressionStream (в двух форматах: deflate-raw и brotli). Для замеров я выбрал две JSON-схемы: GitHub Funding и JSON Resume. Длину URL считал после сжатия и кодирования, а время сжатия и распаковки измерил с помощью Vitest Benchmarking в Browser Mode.
Длина закодированного состояния
Размеры схем в символах после кодирования и сжатия:
Метод | GitHub Funding | JSON Resume |
|---|---|---|
без сжатия | 2 248 (100%) | 8 833 (100%) |
pako | 1 112 (49%) | 3 043 (34%) |
fflate | 1 112 (49%) | 3 095 (35%) |
lz-string | 1 729 (77%) | 5 125 (58%) |
CompressionStream deflate-raw | 1 099 (49%) | 3 004 (34%) |
CompressionStream brotli | 888 (40%) | 2 295 (26%) |
На выбранных схемах lz-string дал заметно более длинный результат, чем остальные варианты. Близкие результаты pako, fflate и CompressionStream с deflate-raw закономерны: все три используют кодек DEFLATE.
Скорость сжатия и распаковки


В этих замерах lz-string уступил альтернативам на основе DEFLATE: результат получился длиннее, а сжатие и распаковка заняли больше времени. Поэтому дальше я его не рассматриваю.
brotli в CompressionStream заметно медленнее сжимает данные, а его поддержка браузерами пока ограничена. Его стоит выбирать, только если минимальная длина URL критически важна и целевые браузеры поддерживают этот формат.
Размер зависимости
CompressionStream встроен в браузер, поэтому не добавляет зависимостей в клиентский бандл. Размер fflate после gzip-сжатия составляет 4,61 КБ, а pako – 15 КБ. При почти одинаковой скорости и коэффициенте сжатия fflate выглядит предпочтительнее.
Выбор метода сжатия
Остаются два практичных варианта с сопоставимыми скоростью и коэффициентом сжатия: CompressionStream с deflate-raw и fflate.
CompressionStream не увеличивает клиентский бандл, но требует поддержки API в браузере. fflate работает без нативного API и даёт больше контроля над сжатием, в том числе позволяет использовать собственные словари.
Для Schemagic я выбрал fflate. Дополнительно я использую префикс #dr:... для обозначения формата. Это позволяет позднее перейти на другой формат, не нарушив работу существующих ссылок.
Подводим итог
Прежде чем сжимать состояние и записывать его в URL, убедитесь, что такой способ хранения подходит для вашего сценария. Если данных немного, лучше использовать читаемые query-параметры.
Для большого объёма данных:
Храните данные во фрагменте URL, чтобы они не отправлялись на сервер.
Сериализуйте данные, сожмите их подходящим способом, а затем закодируйте результат в Base64url.
При частых изменениях обновляйте URL после небольшой задержки и используйте
history.replaceState(), чтобы не создавать новую запись в истории при каждой правке.Если формат сжатия может измениться, обозначьте его во фрагменте, чтобы сохранить совместимость с уже опубликованными ссылками.

