اگر با Node.js کار کرده باشید، احتمالاً بارها شنیدید که یکی از بزرگ‌ترین مزیت‌هاش اینه که توی کارهای I/O خیلی سریع و سبک عمل میکنه و میتونه تعداد زیادی Request رو بدون اینکه برای هرکدوم یه Thread جدا بسازه مدیریت کنه. این حرف درسته، ولی یه سوءتفاهم خطرناک هم پشتش وجود داره؛ خیلی‌ها فکر میکنن چون Node.js در مدیریت درخواست‌ها سریعه، پس هر کاری که داخل یه Request انجام بدیم هم خودبه‌خود سریع و غیرمسدودکننده‌ست.

اینجاست که یه تابع کاملاً ساده مثل JSON.parse() میتونه غافلگیرتون کنه.

فرض کنید کاربر یه JSON خیلی بزرگ برای API شما میفرسته، یا از یه سرویس خارجی چند ده مگابایت JSON دریافت میکنید و میخواید اون رو تبدیل به Object کنید. در نگاه اول کار خاصی نیست؛ یه JSON.parse() میزنیم و تمام. اما اگر حجم داده زیاد باشه، همین عملیات میتونه Event Loop رو برای مدت قابل توجهی درگیر کنه و در اون مدت Requestهای دیگه مجبور بشن منتظر بمونن.

چرا JSON.parse میتونه Node.js رو قفل کنه؟

دلیل اصلی خیلی ساده‌ست؛ JSON.parse() یه عملیات Synchronous هست. یعنی وقتی Node.js شروع میکنه یه رشته JSON رو Parse کنه، تا زمانی که کارش تموم نشده اجرای JavaScript دیگه‌ای روی همون Event Loop انجام نمیشه.

برای یه JSON چند کیلوبایتی این موضوع اصلاً مهم نیست و احتمالاً حتی متوجهش هم نمیشید، ولی وقتی حجم داده به چند ده یا چند صد مگابایت برسه، Parse کردنش میتونه زمان قابل توجهی از CPU بگیره.

یه تست ساده میتونه موضوع رو مشخص کنه:

const bigObject = {
  users: Array.from({ length: 1_000_000 }, (_, i) => ({
    id: i,
    name: `user-${i}`,
    active: true
  }))
};

const json = JSON.stringify(bigObject);

console.time('parse');
const result = JSON.parse(json);
console.timeEnd('parse');

روی سیستم‌های مختلف زمان اجرای این کد فرق میکنه، ولی نکته اصلی عدد دقیق نیست؛ مسئله اینه که در زمان اجرای JSON.parse()، Event Loop مشغوله و بقیه کدهای JavaScript باید صبر کنن.

Event Loop چرا اینقدر مهمه؟

Node.js معمولاً یه Event Loop اصلی داره که وظیفه اجرای کد JavaScript رو بر عهده داره. قدرت Node.js اینه که وقتی مثلاً منتظر دیتابیس، فایل یا پاسخ شبکه هستیم، Event Loop بیکار نمیمونه و میره سراغ Request بعدی.

ولی اگه خودمون یه عملیات سنگین CPU بهش بدیم، دیگه جایی برای فرار نداره.

مثلاً تصور کنید API شما در حالت عادی هر Request رو در ۳۰ میلی‌ثانیه پاسخ میده، ولی ناگهان یه کاربر یه JSON بزرگ میفرسته که Parse کردنش دو ثانیه طول میکشه. در اون دو ثانیه ممکنه ده‌ها یا صدها Request دیگه وارد سرور شده باشن، ولی همه باید منتظر بمونن تا اون Parse تموم بشه.

یعنی یه Request سنگین میتونه تجربه همه کاربرهای دیگه رو خراب کنه.

JSON.stringify هم میتونه همین بلا رو سرتون بیاره

این مشکل فقط مخصوص JSON.parse() نیست. JSON.stringify() هم Synchronous هست و اگر یه Object خیلی بزرگ رو تبدیل به JSON کنید، CPU و Event Loop رو درگیر میکنه.

مثلاً یه Endpoint دارید که یه گزارش خیلی بزرگ از دیتابیس میگیره و بعد اون رو به JSON تبدیل میکنه تا برای کاربر بفرسته. ممکنه Query دیتابیس خیلی سریع انجام بشه، ولی مرحله تبدیل Object به JSON خودش تبدیل به گلوگاه اصلی سیستم بشه.

این مسئله مخصوصاً توی گزارش‌های حجیم، Export داده، APIهای تحلیلی و سیستم‌هایی که Objectهای خیلی بزرگ تولید میکنن بیشتر دیده میشه.

اولین راه‌حل: اجازه ندید Payload بی‌نهایت بزرگ وارد سرور بشه

خیلی وقت‌ها بهترین راه‌حل این نیست که دنبال تکنیک عجیب بگردیم؛ باید از همون ورودی جلوی مشکل رو بگیریم.

اگر API شما منطقی نیست که JSON صد مگابایتی دریافت کنه، اصلاً اجازه ندید چنین داده‌ای وارد برنامه بشه. مثلاً در Express میشه محدودیت Body تعریف کرد:

import express from 'express';

const app = express();

app.use(express.json({
  limit: '2mb'
}));

app.post('/users', (req, res) => {
  res.json({ success: true });
});

با همین تنظیم ساده، Requestهایی که Body بیش از حد بزرگ دارن قبل از اینکه تبدیل به یه مشکل جدی بشن Reject میشن. مقدار مناسب این Limit هم به پروژه شما بستگی داره؛ برای بعضی APIها حتی ۱ مگابایت زیاده و برای بعضی سیستم‌ها ممکنه چند مگابایت کاملاً طبیعی باشه.

Stream کردن همیشه بهتر از خوردن کل فایل یه‌جاست

فرض کنید یه فایل JSON خیلی بزرگ دارید و واقعاً مجبورید اون رو پردازش کنید. یکی از بدترین کارها اینه که کل فایل رو یکجا داخل RAM بخونید و بعد یه JSON.parse() عظیم روش اجرا کنید.

برای داده‌های خیلی بزرگ بهتره تا جای ممکن از Streaming استفاده کنید؛ یعنی داده رو تکه‌تکه بخونید و پردازش کنید، نه اینکه کلش رو یه‌جا وارد حافظه کنید.

البته JSON معمولی برای Streaming خیلی ایده‌آل نیست و گاهی فرمت‌هایی مثل NDJSON یا پردازش Chunk-based انتخاب بهتری هستن، چون میشه هر رکورد رو جداگانه خوند و پردازش کرد.

اگه واقعاً پردازش سنگینه، Event Loop رو درگیر نکن

بعضی وقت‌ها حجم داده واقعاً زیاده و نمیشه با یه Limit ساده مسئله رو حل کرد. مثلاً یه سیستم Data Processing دارید که باید فایل‌های بزرگ رو تحلیل کنه.

اینجا بهتره پردازش سنگین رو از Event Loop اصلی خارج کنید و ببرید سمت Worker Threads یا یه Job Queue جدا. اینطوری حتی اگر پردازش چند ثانیه طول بکشه، API اصلی همچنان میتونه Requestهای کاربران رو پاسخ بده.

اصل ماجرا اینه که Event Loop باید تا جای ممکن سبک بمونه؛ هرچی عملیات CPU-Heavy بیشتری روی اون اجرا کنید، Node.js بیشتر شبیه یه سرور تک‌صفحه‌ای خسته میشه که همه باید پشت یه کار سنگین منتظر بمونن.

از کجا بفهمیم مشکل از JSON بزرگه؟

یکی از نشونه‌های واضح اینه که CPU ناگهان بالا میره ولی دیتابیس یا سرویس خارجی مشکلی نداره. همچنین ممکنه Latency تمام Endpointها همزمان زیاد بشه، حتی Endpointهایی که هیچ ارتباطی با اون Request سنگین ندارن.

اگه یه Endpoint خاص فقط وقتی داده بزرگ دریافت میکنه باعث کند شدن کل برنامه میشه، بهتره اندازه Payload، زمان JSON.parse() یا JSON.stringify() و وضعیت Event Loop رو بررسی کنید.

گاهی مشکل دقیقاً جلوی چشممونه ولی چون کد فقط یه خطه، اصلاً بهش شک نمیکنیم.

جمع بندی

JSON.parse() یه تابع بد یا خطرناک نیست و تقریباً در تمام پروژه‌های JavaScript استفاده میشه، اما مثل خیلی از ابزارهای دیگه وقتی حجم داده زیاد میشه رفتارش اهمیت پیدا میکنه. Parse کردن یه JSON کوچیک هیچ مشکلی ایجاد نمیکنه، ولی یه Payload خیلی بزرگ میتونه Event Loop رو درگیر کنه و باعث بشه Requestهای بقیه کاربران هم پشت اون معطل بمونن.

بهترین کار اینه که اندازه ورودی‌ها رو محدود کنید، داده‌های خیلی بزرگ رو به صورت Stream یا تکه‌تکه پردازش کنید و اگر واقعاً با پردازش CPU-Heavy طرف هستید، کار رو از Event Loop اصلی خارج کنید.

پس دفعه بعد که یه Endpoint کند شد، فقط دیتابیس و شبکه رو مقصر ندونید؛ شاید قاتل Performance دقیقاً همین یه خط ساده باشه:

JSON.parse(...)