Ни для кого не секрет, что JSON широко используется в веб-разработке: обмен данными между клиентом (браузером) и сервером, хранение в NoSQL-базах, конфигурационные файлы, API-ответы и многое другое. Он стал практически родным форматом данных для JavaScript и Node.js. Однако при работе с JSON стоит учитывать ряд ограничений и подводных камней, которые в больших проектах могут вылиться в серьёзные проблемы с производительностью, точностью и безопасностью.
В этой статье мы разберём:
-
Неочевидные проблемы при сериализации/десериализации JSON — с фокусом на веб-разработку.
-
Обработку больших JSON-файлов — нюансы и инструменты в Node.js (и не только).
-
Популярные альтернативы JSON: MessagePack и Protocol Buffers — когда и как их стоит применять в веб-приложениях.
Статья рассчитана на веб-разработчиков, которые работают с JSON каждый день и хотят глубже разобраться в его особенностях, а также расширить свой стек инструментов.
Важно отметить, что описанные в статье проблемы не являются исключительной особенностью JSON — они присущи любым текстовым форматам (XML, YAML и др.). Мы сосредоточились на JSON как на самом популярном варианте в веб-разработке и Node.js-экосистеме, но все приведённые грабли встречаются и в других форматах.
Неочевидные проблемы при сериализации и десериализации JSON (веб-контекст)
Большие числа и потеря точности
В JavaScript (и, соответственно, в браузере и Node.js) максимальное безопасное целое число равно 2^53 — 1. Если вы храните ID или денежные суммы, которые превышают этот порог, то при парсинге JSON может произойти потеря точности.
const jsonString = '{"order_id": 1234567890123456789, "price": 1499.95}'; const data = JSON.parse(jsonString); console.log(data.order_id); // 1234567890123456800 — ошибка, хвост числа "округлился"
Рекомендации:
-
Храните слишком большие целые числа в JSON как строки:
{"order_id": "1234567890123456789"}
. -
В Node.js (начиная с версии 10.4, а также в современных браузерах) можно использовать BigInt для точных вычислений:
BigInt("1234567890123456789")
. Но в JSON по-прежнему придётся обрабатывать как строку. -
Если нужно передавать огромные суммы/балансы в финансовом контексте, рассмотрите передачу в строчном формате либо используйте специализированные решения (двоичные протоколы, см. раздел про ProtoBuf).
Даты и время
Стандарт JSON не содержит встроенного типа даты/времени. В веб-среде чаще всего встречаются три подхода:
-
ISO 8601:
2025-01-04T12:34:56Z
-
Unix Timestamp (в секундах или миллисекундах):
1672822561000
-
Пользовательские форматы:
04/01/2025 12:34:56
,2025.01.04 12:34:56
и т.д.
Проблемы возникают, когда мы забываем учитывать:
-
Браузер хранит дату внутри объекта
Date
в формате UTC, но при выводе преобразует её в локальный часовой пояс, что может вызывать путаницу при обработке времени. -
Несогласованность форматов. Например, сервер выдаёт
"2025-01-04T12:34:56Z"
, а кто-то пытается парсить его какMM/DD/YYYY
. -
Некоторые библиотеки или операции могут обрезать миллисекунды из даты, что приводит к расхождению данных между различными системами.
Как решать:
-
Соблюдайте единый формат для дат, чаще всего используют ISO 8601 (UTC).
-
На клиенте используйте проверенные библиотеки: date-fns, Moment.js (находится в режиме поддержания и не развивается ), Day.js, Luxon.
-
Также обратите внимание на новый Temporal API (пока в стадии черновика), который потенциально может заменить Date в JavaScript.
-
-
На сервере (Node.js) для хранения и обработки дат в базе данных (например, PostgreSQL, MongoDB) старайтесь приводить всё к UTC.
Экранирование спецсимволов, Unicode и эмодзи
JSON-строки должны экранировать спецсимволы (\n
, \t
, \"
, \\
). Если нужно передавать эмодзи или символы за пределами U+FFFF
, то фактически они превращаются в суррогатные пары (например, \ud83d\ude00
для 😀).
В большинстве случаев это прозрачно для веб-разработчика, но могут возникнуть проблемы:
-
Если JSON-строка несколько раз проходит через процесс сериализации, каждый этап добавляет обратные слеши, что может привести к накоплению экранированных символов и, в конечном итоге, к сложночитаемому «лесу» слешей.
-
Неверная работа с Unicode на этапах парсинга сторонними библиотеками или плагинами.
const data = { text: "Hello\nNewLine", emoji: "😀" }; const str = JSON.stringify(data); console.log(str); // {"text":"Hello\nNewLine","emoji":"\ud83d\ude00"} const parsed = JSON.parse(str); console.log(parsed); // { text: 'Hello\nNewLine', emoji: '😀' }
Обычно всё хорошо, если использовать стандартный JSON.parse
/JSON.stringify
, но если у вас сложный пайплайн (например, преобразование на стороне клиентских библиотек, несколько слоёв API), стоит убедиться, что все компоненты корректно обрабатывают экранирование.
Производительность
Веб-разработчики, создавая REST API, часто генерируют JSON-ответы на десятки мегабайт, а потом удивляются долгому парсингу и высокому расходу памяти.
-
На стороне клиента:
JSON.parse
крупного ответа (особенно в мобильном браузере) может подвесить UI на несколько секунд. -
На стороне сервера (Node.js):
JSON.stringify
очень большого объекта тоже затратен.
Если ваше веб-приложение возвращает огромные JSONы:
-
Подумайте, действительно ли нужно отдавать всё сразу. Возможно, лучше сделать пагинацию, lazy-load или частичные данные.
-
Используйте стриминг там, где это уместно (Node.js
stream
+ JSON chunking). -
Сжимайте ответ (например,
gzip
илиbrotli
в Express через compression).
Обработка больших JSON-файлов на Node.js
Стриминг и потоки (Streams API)
В Node.js крайне желательно обрабатывать большие JSON-файлы (или потоки данных) поэтапно, а не загружать их полностью в память.
-
SAX-подобные парсеры для JSON: stream-json, JSONStream.
-
Чтение файл → парсер → обработка: вы считываете файл через
fs.createReadStream
, передаёте в стриминговый парсер, который эмитит объекты по мере чтения.
Пример (используя stream-json
):
const fs = require('fs'); const { parser } = require('stream-json'); const { streamValues } = require('stream-json/streamers/StreamValues'); const readStream = fs.createReadStream('big.json'); readStream .pipe(parser()) .pipe(streamValues()) .on('data', (data) => { // data.value содержит очередной кусок JSON console.log(data.value); }) .on('end', () => { console.log('Done processing large JSON!'); });
Таким образом, Node.js не хранит весь JSON в памяти, а обрабатывает по частям.
NDJSON / JSON Lines
Если у вас много однотипных объектов, рассмотрите формат NDJSON (Newline-Delimited JSON). Каждая строка — отдельный JSON-объект:
{"id":1,"name":"Item1"} {"id":2,"name":"Item2"} {"id":3,"name":"Item3"}
Читать такой файл через стримы в Node.js очень удобно. Можно построчно обрабатывать:
const fs = require('fs'); const readline = require('readline'); async function processNDJSON(filePath) { const fileStream = fs.createReadStream(filePath); const rl = readline.createInterface({ input: fileStream }); for await (const line of rl) { const obj = JSON.parse(line); // Обработка obj console.log(obj.name); } } processNDJSON('items.ndjson');
Преимущества:
-
Не надо парсить огромный массив, можно сразу по строкам.
-
Если данные идут в реальном времени (например, лог-сервис), то NDJSON позволяет обрабатывать их на лету.
Компрессия
Для экономии места и ускорения передачи больших JSON-ответов по HTTP в продакшене практически всегда включают gzip или brotli-сжатие:
const express = require('express'); const compression = require('compression'); const app = express(); app.use(compression()); // Включаем сжатие app.get('/api/data', (req, res) => { const bigObject = generateHugeObject(); res.json(bigObject); }); app.listen(3000, () => { console.log('Server running...'); });
На клиенте браузер автоматически декомпрессирует ответ, остаётся лишь распарсить JSON. Если же ваш сервис передаёт большие JSON-файлы другой системе, убедитесь, что другая сторона тоже умеет декомпрессировать (обычно это стандарт).
Альтернативы JSON: MessagePack и Protocol Buffers для веб
Это двоичный формат, который сохраняет структуру данных, похожую на JSON (объекты, массивы, строки, числа), но в более компактном виде.
Плюсы:
-
Меньший размер данных (на 20-50% меньше по сравнению с JSON).
-
Высокая скорость парсинга (не нужно разбирать текст).
-
Поддерживается во многих языках, включая JavaScript/Node.js (msgpack5).
Минусы:
-
Меньшая человеко-читаемость (в браузере не так удобно дебажить).
-
Нужно сторонними средствами смотреть, что внутри (нужен декодер).
Пример (Node.js с msgpack5):
const msgpack = require('msgpack5')(); const data = { user: 'Alice', age: 30, scores: [10, 20, 30] }; const packed = msgpack.encode(data); console.log('Packed buffer:', packed); const unpacked = msgpack.decode(packed); console.log('Unpacked:', unpacked);
В реальном проекте MessagePack может дать выигрыш в скорости и объёме передаваемых данных, особенно когда речь идёт о высоконагруженных сервисах.
Protocol Buffers (Protobuf) — двоичный формат от Google, в котором обязательно описывать структуру данных в .proto
-файле (схема). На базе этой схемы генерируется код (классы) для различных языков.
Плюсы:
-
Высокая производительность (парсинг, размер данных).
-
Строгая типизация и версионирование.
-
Идеально подходит для микросервисов на gRPC.
Минусы:
-
Нужно поддерживать
.proto
-схему и генерировать код. -
Меньшая гибкость в сравнении с JSON (сложно передавать «произвольные» структуры).
Упрощенный пример. Схема (user.proto
):
syntax = "proto3"; message User { string name = 1; int32 age = 2; repeated int32 scores = 3; }
Устанавливаем protoc и плагин для Node.js, генерируем JS-код. В итоге получаем файлы вида user_pb.js
.
const messages = require('./user_pb'); // сгенерированный код const user = new messages.User(); user.setName('Alice'); user.setAge(30); user.setScoresList([10, 20, 30]); const bytes = user.serializeBinary(); console.log('Binary length:', bytes.length); const user2 = messages.User.deserializeBinary(bytes); console.log(user2.getName(), user2.getAge(), user2.getScoresList());
Применять Protobuf в веб-разработке имеет смысл, если вы строите масштабируемую систему микросервисов на gRPC или действительно заботитесь о каждом килобайте и миллисекунде. Однако для большинства веб-API, где удобнее быстро смотреть структуру в сыром виде, JSON остаётся основным форматом.
Заключение и рекомендации для веб-разработчиков
-
Проверяйте точность чисел — не полагайтесь на то, что большие
order_id
илиbalance
всегда поместятся в Number. -
Храните и передавайте даты в едином формате (ISO 8601 с UTC) — чтобы избежать путаницы с часовыми поясами.
-
Стримьте большие JSON — Node.js позволяет легко обрабатывать файлы и потоки, не загружая всё в память.
-
Используйте компрессию (
gzip
,brotli
) — это ускорит передачу JSON через HTTP. -
Рассмотрите альтернативы (MessagePack, Protobuf), если у вас высокие требования к производительности и объёму трафика и вы готовы поддерживать двоичный формат (особенно когда надо экономить ресурсы).
Вместо послесловия
В большинстве веб-проектов JSON остаётся удобным и достаточным решением. Однако стоит помнить о его подводных камнях: потеря точности чисел, отсутствие встроенной даты, большие объёмы данных, которые могут убить производительность и съесть всю память. Если вы развиваете сложный сервис с высоконагруженными компонентами, возможно, стоит присмотреться к более эффективным форматам, вроде MessagePack или ProtoBuf. Но для подавляющего большинства случаев JSON в связке с Node.js (и правильным стримингом/компрессией) будет надёжным выбором.
Удачной веб-разработки!
ссылка на оригинал статьи https://habr.com/ru/articles/871616/
Добавить комментарий