JSON Toolkit

JSON মেমরি খাতা: স্ট্রিং, অবজেক্ট ও বড় অ্যারে আসলে কত খরচ করে

সংক্ষেপে: ১০MB JSON ফাইল V8 হিপে ৩০–৫০MB হতে পারে। স্ট্রিং, অবজেক্ট ও সংখ্যার প্রকৃত খরচ, process.memoryUsage দিয়ে বেসলাইন, আর কখন স্ট্রিমিং সাহায্য করে — সবই এখানে।

উন্নত~11 মিনিট পড়াপ্রকাশ 2026-10-31আপডেট 2026-10-31
JSON মেমরি খাতা: V8-এ স্ট্রিং, অবজেক্ট ও অ্যারের হিপ খরচ

মূল কথা: আগে process.memoryUsage দিয়ে বেসলাইন করুন, টেক্সট ও অবজেক্ট গ্রাফকে দুটি আলাদা বৃদ্ধি হিসেবে রেকর্ড করুন, তারপর আর কিছু করার আগে একটি পূর্ণ কপি সরান — কোনো JSON.parse(JSON.stringify(x)) নয়, কাঁচা ডকুমেন্টের প্রতিক্রিয়াশীল মোড়ক নয়। এরপর দীর্ঘজীবী রেফারেন্স কাটুন, আর স্ট্রিমিং সবশেষে ভাবুন: এর সমতল মেমরি বক্ররেখা র‍্যান্ডম অ্যাক্সেস হারানোর যোগ্য কেবল যখন ফাইল ধরে না বা আপনার দরকার মাত্র কয়েকটি ফিল্ড।

একটি ১০MB JSON ডকুমেন্ট JSON.parse-এর পর হিপে ৩০MB থেকে ৫০MB হতে পারে, আর ক্ষণস্থায়ী শিখর আরও উঁচুতে যায়। এই হিসাব না করে "অপ্টিমাইজেশন" নিছক অনুমান।

১. টেক্সট ও অবজেক্ট গ্রাফ দুটি আলাদা খাতা

ডিস্কে ও রিকোয়েস্ট বডিতে JSON বাইটের ক্রম। মেমরিতে ঢোকার পর এটি দুটি জিনিস হয়ে যায়: একটি স্ট্রিং, এবং অবজেক্ট, অ্যারে, সংখ্যা ও বুলিয়ানে গড়া অবজেক্ট গ্রাফ। এই দুই খাতা আলাদা রাখাই ব্যাখ্যা করার একমাত্র পথ কেন ১০MB ফাইল একটি প্রসেসকে ৮০MB খাওয়াতে পারে।

ডেটার আকৃতি অবজেক্ট গ্রাফ বনাম টেক্সট আকার প্রধান খরচ
অবজেক্টের সমতল অ্যারে (প্রতিটি প্রায় ৮ ফিল্ড) মোটামুটি ২.৫–৪ গুণ প্রতি এলিমেন্টে একটি অবজেক্ট হেডার ও প্রপার্টি স্লট
গভীরভাবে নেস্টেড কনফিগারেশন মোটামুটি ৩–৫ গুণ অনেক ছোট অবজেক্ট, প্রতিটির নিজস্ব হেডার
খুব দীর্ঘ স্ট্রিংয়ের অ্যারে মোটামুটি ১.২–২ গুণ স্ট্রিং হেডার প্লাস অক্ষর ডেটা
দশমিকযুক্ত বড় সংখ্যা মোটামুটি ২–৩ গুণ প্রতি মানে একটি HeapNumber
অনেক পুনরাবৃত্ত কি-যুক্ত ডকুমেন্ট কি-সংখ্যার সমানুপাতিক পার্সিংয়ে কি হ্যাশিং ও প্রপার্টি সেটআপ

এই গুণকগুলো মাত্রার ক্রমের রেফারেন্স যা V8 সংস্করণ, পয়েন্টার কম্প্রেশন, ফিল্ড সংখ্যা ও স্ট্রিং-কন্টেন্টের উপর নির্ভর করে। টেবিলটি একটি কথা বলে: অবজেক্ট গ্রাফের আকার "নোড সংখ্যা × প্রতি-নোড ওভারহেড" দিয়ে হয়, টেক্সটের বাইট সংখ্যা দিয়ে নয়।

২. স্ট্রিং: সবচেয়ে কম গণ্য খরচের লাইন

V8 স্ট্রিং নিছক অক্ষরের অ্যারে নয়। এটি অবজেক্ট হেডার বহন করে (একটি ম্যাপ পয়েন্টার, একটি হ্যাশ ফিল্ড ও একটি দৈর্ঘ্য), যা পয়েন্টার কম্প্রেশনসহ সাধারণ ৬৪-বিট বিল্ডে অক্ষরের আগেই প্রায় এক ডজন বাইট খরচ করে। অক্ষর দুটি উপায়ের একটিতে রাখা হয়:

  • এক-বাইট স্ট্রিং Latin-1 রেঞ্জের অক্ষরের জন্য: প্রতি অক্ষরে ১ বাইট;
  • দুই-বাইট স্ট্রিং যখন কন্টেন্টে CJK অক্ষর, ইমোজি বা Latin-1-এর বাইরের কিছু থাকে: প্রতি অক্ষরে ২ বাইট।

একটি চীনা অক্ষর UTF-8 ফাইলে ৩ বাইট নেয় কিন্তু দুই-বাইট স্ট্রিংয়ে প্রতি অক্ষরে ২ বাইট, প্লাস হেডার। টেক্সট আকার ও হিপ আকার এক মাপ নয়, আর তারা সবসময় এক দিকে চলে না।

JSON কাজের জন্য সরাসরি তিনটি বিষয় গুরুত্বপূর্ণ:

  1. স্লাইস প্যারেন্টকে জীবিত রাখে। V8 দীর্ঘতর উপ-স্ট্রিংকে SlicedString হিসেবে প্রকাশ করে, যা প্যারেন্ট রেফারেন্স প্লাস অফসেট ও দৈর্ঘ্য রাখে। ৮০MB লগ থেকে কাটা ২০০ অক্ষর রাখলে পুরো ৮০MB পৌঁছানোর যোগ্য হয়ে থাকে যতদিন সেই স্লাইস ছাড়া না হয়।
  2. কনক্যাটেনেশন ConsString তৈরি করে। বড় স্ট্রিংয়ে বারবার += ফ্র্যাগমেন্টের একটি গাছ বানায় যা কেবল অক্ষর সত্যিই পড়ার সময় একটানা মেমরিতে চ্যাপ্টা হয়। দশ হাজারবার কনক্যাট করে একবার পড়লে সেই মুহূর্তে বড় বরাদ্দ লাফ হয়।
  3. পুনরাবৃত্ত মান স্বয়ংক্রিয়ভাবে ভাগ হয় না। "status": "paid" ধারণকারী দশ হাজার রেকর্ড সাধারণত দশ হাজার স্বাধীন স্ট্রিং অবজেক্ট পায়; পার্সার সেগুলোকে একটি ধ্রুবকে নির্দেশ করে না। এই খরচ বাঁচাতে ঘন স্ট্রিং নিজেই এনাম বা সংখ্যায় ম্যাপ করতে হবে।

দীর্ঘ সময়ের জন্য ছোট অংশ রাখতে হলে সেটিকে স্বাধীন স্ট্রিং বানিয়ে মূল ছাড়ুন:

// Bad: the slice references the whole document, so logText cannot be collected
const summary = logText.slice(0, 200);

// Good: create an independent string, then release logText
const summaryCopy = Buffer.from(summary, 'utf8').toString('utf8');

৩. অবজেক্ট: প্রতিটি কি একটি স্লট নেয়

সাধারণ অবজেক্টে অন্তত তিনটি জিনিস থাকে: তার লুকানো ক্লাসের (ম্যাপ) পয়েন্টার, প্রপার্টি স্টোরের পয়েন্টার, আর এলিমেন্ট স্টোরের পয়েন্টার। পয়েন্টার কম্প্রেশনসহ প্রতিটি পয়েন্টার ৪ বাইট, আর হিপ অবজেক্ট হেডার আরও যোগ করে, তাই খালি অবজেক্টও প্রায় ষোলো থেকে বিশ-কিছু বাইট খরচ করে। পয়েন্টার কম্প্রেশন ছাড়া ৬৪-বিট বিল্ডে দ্বিগুণ।

নামযুক্ত প্রপার্টি দুই জায়গার একটিতে পড়ে:

  • ইন-অবজেক্ট প্রপার্টি: অবজেক্ট হেডারের ঠিক পরে সাজানো স্লট, যা দ্রুততম অ্যাক্সেস দেয়;
  • একটি PropertyArray: ইন-অবজেক্ট স্লট শেষ হলে প্রপার্টি আলাদা অ্যারেতে সরে যায় আর অবজেক্ট সেটিতে পয়েন্টার রাখে।

প্রতিটি স্লট ৪ বা ৮ বাইট, আর কি স্ট্রিং আলাদাভাবে গোনা হয়। এজন্যই দীর্ঘ ফিল্ড-নাম শত শত হাজার রেকর্ডে প্রকৃত খরচ হয়ে ওঠে, নিছক নান্দনিক বিষয় নয়।

সুখবর হলো লুকানো ক্লাস ভাগ হয়: একই ক্রমে একই কি-যুক্ত এক মিলিয়ন অবজেক্ট একটি ম্যাপ ভাগ করে, কাঠামোগত তথ্য কপি করে না। দুঃসংবাদ হলো এই প্যাটার্নগুলো সেই ভাগ ভাঙে বা অবজেক্টকে দামি প্রকাশে ঠেলে দেয়:

  • কিছু রেকর্ডে remark ফিল্ড আছে আবার অন্যগুলোয় নেই, ফলে দুই আকৃতি আর প্রতিটি অ্যাক্সেসে অতিরিক্ত আকৃতি-চেক;
  • ভিন্ন ইনসার্শন ক্রম ভিন্ন আকৃতি দিতে পারে;
  • খুব বড় প্রপার্টি সংখ্যা (প্রায় এক হাজারের বেশি) বা ঘন delete অবজেক্টকে ডিকশনারি মোডে ঠেলে, যেখানে প্রতিটি প্রপার্টি হ্যাশ-টেবিল এন্ট্রিও বহন করে;
  • Object.create(null) জন্ম থেকেই ডিকশনারি মোডে। এটি ভালো লুকআপ টেবিল আর হট লুপে অ্যাক্সেস করা নির্দিষ্ট-স্কিমা রেকর্ডের জন্য দুর্বল পছন্দ।

একটি প্রচলিত ভুল সংশোধনের যোগ্য: JSON ডুপ্লিকেট কি অনুমোদন করে, আর {"a":1,"a":2} বৈধ ডকুমেন্ট যার ফল a === 2। চূড়ান্ত অবজেক্ট একটি মান রাখে, কিন্তু পার্সার এখনও প্রতিটি উপস্থিতি স্ক্যান ও অ্যাসাইন করে। ডুপ্লিকেট কি মেমরি বাঁচায় না; কেবল পার্স সময় খরচ করে।

৪. অ্যারে ও সংখ্যা: এলিমেন্ট কাইন্ড প্রস্থ ঠিক করে

V8 অ্যারের জন্য কয়েকটি "এলিমেন্ট কাইন্ড" ট্র্যাক করে, আর প্রতিটির প্রতি-স্লট প্রস্থ আলাদা:

অ্যারের কন্টেন্ট এলিমেন্ট কাইন্ড প্রতি স্লটে আনুমানিক খরচ
ছোট ইন্টিজার (Smi রেঞ্জে) PACKED_SMI_ELEMENTS ৪ বাইট
দশমিক বা খুব বড় ইন্টিজার PACKED_DOUBLE_ELEMENTS ৮ বাইট
স্ট্রিং বা অবজেক্ট PACKED_ELEMENTS ৪-বাইট পয়েন্টার প্লাস মান নিজেই
delete-তৈরি ফাঁকযুক্ত অ্যারে HOLEY_* একই, প্লাস ফাঁক চেক
খুব স্পার্স অ্যারে DICTIONARY_ELEMENTS হ্যাশ এন্ট্রি, সাধারণত ২০+ বাইট

দুটি নিয়ম মনে রাখার যোগ্য:

প্রথমত, এলিমেন্ট কাইন্ড কেবল অবনতি হয়। ছোট ইন্টিজার দিয়ে শুরু হওয়া অ্যারে 0.5 ঢোকালেই ডাবল অ্যারে হয়ে যায়, আর স্ট্রিং ঢোকালে জেনেরিক এলিমেন্ট অ্যারে। এটি কখনো স্বয়ংক্রিয়ভাবে ফেরে না; নতুন অ্যারে বানাতে হয়।

দ্বিতীয়ত, JSON সংখ্যা কোথায় পড়ে তা তার মানের উপর নির্ভর করে। Smi রেঞ্জের (প্রায় প্লাস-মাইনাস ১.০৭ বিলিয়ন) ইন্টিজার সরাসরি Smi হিসেবে রাখা হয় আর একটি পয়েন্টার স্লট নেয়। দশমিক বা সূচকযুক্ত সংখ্যা, বা সেই রেঞ্জের বাইরের মান প্রায় ১৬ বাইটের HeapNumber বরাদ্দ করে, আর অ্যারেতে তার পয়েন্টার রাখা হয়। তাই [1, 2, 3] ও [0.1, 0.2, 0.3] কেবল মানে নয়, "এক ৪-বাইট স্লট" বনাম "৮-বাইট স্লট প্লাস একটি হিপ অবজেক্ট"-এ আলাদা।

এটাই বড়-ইন্টিজার id প্রিসিশন সমস্যার মূলও: নিরাপদ ইন্টিজার রেঞ্জের বাইরে JSON সংখ্যা ডাবল ধরা হয় আর নিম্ন-ক্রমের অঙ্ক সরাসরি হারায়। JSON অ্যারে প্যাকড ও ফাঁকহীন আসে, তাই পার্সের পর এলিমেন্ট delete করবেন না; তাতে পুরো অ্যারে দামি প্রকাশের দিকে ঠেলে যায়।

৫. পার্স ও সিরিয়ালাইজ শিখর: প্রসেস আসলে যেখানে মরে

স্থিতিশীল মেমরি এক খাতা; শিখর আরেক। JSON.parse(text) চলার সময় প্রসেস একসঙ্গে তিন জিনিস ধরে:

  1. ইনপুট স্ট্রিং text;
  2. তৈরি হচ্ছে এমন, এখনও অসম্পূর্ণ অবজেক্ট গ্রাফ;
  3. পার্সারের নিজস্ব সাময়িক কাঠামো।

পার্স শিখর মোটামুটি "টেক্সট + অবজেক্ট গ্রাফ + সাময়িক ওভারহেড"। পার্সের পর text যদি কোনো ভেরিয়েবল, ক্লোজার, মডিউল-স্তরের ক্যাশে বা প্রতিক্রিয়াশীল স্টোরে এখনও রেফারেন্সড থাকে, সেই টেক্সট মেমরি কখনো ফেরে না।

সিরিয়ালাইজেশনেরও শিখর আছে। JSON.stringify(data) আউটপুট স্ট্রিং বানানোর সময় পুরো অবজেক্ট গ্রাফ ধরে, মাঝে কনক্যাটেনেশন বাফার নিয়ে। আউটপুট V8-এর স্ট্রিং দৈর্ঘ্য সীমার (প্রায় ৫০০ মিলিয়ন অক্ষর) কাছে গেলে RangeError দেয়; এই ব্যর্থতা সাধারণত পুরো ডেটাবেস এক JSON ডকুমেন্টে এক্সপোর্ট করা স্ক্রিপ্টে দেখা যায়।

সবচেয়ে দামি প্রচলিত প্যাটার্ন সিরিয়ালাইজেশনকে গভীর ক্লোন হিসেবে ব্যবহার করে:

// Peak is roughly: original object graph + temporary text + new object graph
const safeCopy = JSON.parse(JSON.stringify(bigData));

এই একটি লাইন একই ডেটা তিনবার রাখে। রিড-অনলি কাজে রেফারেন্স দিন। বিচ্ছিন্নতা সত্যিই দরকার হলে structuredClone ব্যবহার করুন আর মেনে নিন এটিও পুরো অবজেক্ট গ্রাফ আবার গড়ে।

৬. process.memoryUsage দিয়ে মাপা

Node-এ process.memoryUsage() একসঙ্গে পাঁচটি সংখ্যা ফেরত দেয়: rss (প্রসেসের ব্যবহৃত ভৌত মেমরি), heapTotal, heapUsed (ব্যবহৃত JS হিপ), external (C++ অবজেক্টের ধরা মেমরি) এবং arrayBuffers (ArrayBuffer ও Buffer মেমরি)। JSON হ্যান্ডলিং অনুশীলনে অবজেক্ট গ্রাফের জন্য heapUsed সঠিক লেন্স আর rss বলে পুরো প্রসেস নিয়ন্ত্রণের বাইরে কি না।

মাপার স্ক্রিপ্টে তিনটি শৃঙ্খলা দরকার: আগে global.gc() কল করুন, ধাপে ধাপে নমুনা নিন, আর একবারে একটি চলক বদলান। এই স্ক্রিপ্ট "টেক্সট পড়া" ও "অবজেক্টে পার্স" — দুইয়ের বৃদ্ধি আলাদা রেকর্ড করে:

// Run: node --expose-gc measure-json-memory.mjs
import { readFileSync } from 'node:fs';

global.gc();
const baseline = process.memoryUsage().heapUsed;

const text = readFileSync('big.json', 'utf8');
const afterRead = process.memoryUsage().heapUsed;

const data = JSON.parse(text);
const afterParse = process.memoryUsage().heapUsed;

console.log({
  textOnHeap: afterRead - baseline,
  graphOnHeap: afterParse - afterRead,
  parsePeak: afterParse - baseline,
});

কয়েকটি ফাঁদ:

  • --expose-gc স্পষ্টভাবে চালু করতে হবে, নাহলে global.gc থাকে না;
  • heapUsed কেবল JS হিপ ঢাকে। ফাইল-পড়া Buffer external ও arrayBuffers-এ থাকে, তাই একটি সংখ্যা দিয়ে সব ব্যাখ্যা করবেন না;
  • নমুনার মাঝে বড় অবজেক্ট console.log করবেন না; এটি সাময়িক রেফারেন্স জীবিত রাখে;
  • প্রথম রানে JIT ওয়ার্ম-আপ ও lazy কম্পাইলেশন থাকে। অন্তত তিন রাউন্ড চালিয়ে মধ্যমা নিন;
  • ব্রাউজারে তুলনা করতে DevTools মেমরি প্যানেলে হিপ স্ন্যাপশট নিয়ে Retained Size পড়ুন, Allocation Timeline-এর করাত-দাঁতের দিকে তাকিয়ে না থেকে।

এই বেসলাইন ছাড়া "স্ট্রিমিং-এ গেলে ঠিক হয়ে যাবে" বা "ক্যাশে দিলে দ্রুত হবে" দাবি নিছক অনুভূতি।

৭. স্ট্রিমিং শিখর মেমরি কেনে, মোট সময় নয়

স্ট্রিমিং পার্সিংয়ের লেনদেন স্পষ্ট: ফাইল আকারের সঙ্গে আর না বাড়া মেমরির বিনিময়ে র‍্যান্ডম অ্যাক্সেস।

পদ্ধতি শিখর মেমরি র‍্যান্ডম অ্যাক্সেস উপযুক্ত
পূর্ণ JSON.parse টেক্সট প্লাস সম্পূর্ণ অবজেক্ট গ্রাফ হ্যাঁ প্রায় ১০০MB-এর নিচে ও বেশিরভাগ ফিল্ড দরকার
stream-json-এর মতো SAX পার্সার একটি চাঙ্ক প্লাস চলতি এলিমেন্ট না বিশাল ফাইল, বা মাত্র কয়েকটি ফিল্ড দরকার
লাইন-ধরে JSON Lines এক লাইনের অবজেক্ট না স্বাভাবিকভাবেই রেকর্ডের ব্যাচ হওয়া ডেটা
বাইরের ইনডেক্স বা ডেটাবেস কিছুই সম্পূর্ণ লোড হয় না হ্যাঁ একই ডেটার উপর বারবার কুয়েরি
import { createReadStream } from 'node:fs';
import { parser } from 'stream-json';
import { pick } from 'stream-json/filters/Pick';
import { streamArray } from 'stream-json/streamers/StreamArray';

const pipeline = createReadStream('huge.json')
  .pipe(parser())
  .pipe(pick({ filter: 'items' }))
  .pipe(streamArray());

for await (const { value } of pipeline) {
  handleItem(value);
}

এই কোড কখনো পূর্ণ ট্রি গড়ে না, তাই মেমরি বক্ররেখা সমতল থাকে। খরচও বাস্তব: এড়ানো কোনো ফিল্ডে ফিরে যেতে পারেন না, বৈশ্বিক অ্যাগ্রিগেশন কঠিন হয়ে যায়, এরর রিকভারি আপনার দায়িত্ব, আর বিশুদ্ধ-JS অক্ষর-ধরে-অক্ষর স্ক্যান সাধারণত নেটিভ JSON.parse-এর চেয়ে এক মাত্রা ধীর।

সিদ্ধান্ত "ফাইলটি কি ১০MB ছাড়িয়েছে" নয়। দুটি প্রশ্ন করুন: পার্সের পর আমার বেশিরভাগ ফিল্ড দরকার কি? অনুক্রমিক অ্যাক্সেস নিয়ে বাঁচতে পারি? দুই উত্তরই হ্যাঁ হলে পূর্ণ পার্সিং সঠিক আর আপনার পরিশ্রম কপি ও রেন্ডারিং কমাতে যাওয়া উচিত। যেকোনো একটি না হলে স্ট্রিমিং ভাবুন। সম্পূর্ণ স্তরভিত্তিক কৌশলের জন্য দেখুন বড় JSON পার্সিং, রেন্ডারিং ও স্ট্রিমিং।

৮. কম কপি, আগে ছাড়া

মেমরি কাজে একটি পূর্ণ কপি সরানো প্রায়ই পার্সার বদলানোর চেয়ে বেশি মূল্যবান:

  • রিড-অনলি কাজে রেফারেন্স দিন। "নিরাপত্তার জন্য" গভীর ক্লোন করবেন না, বিশেষত JSON.parse(JSON.stringify(x)) দিয়ে নয়;
  • কাঁচা ডকুমেন্ট প্রতিক্রিয়াশীল সিস্টেমে মুড়বেন না। Vue-র reactive পুরো অবজেক্ট গ্রাফ পুনরাবৃত্তভাবে প্রক্সি করে, আর প্রক্সি স্তরের নিজস্ব মেমরি খরচ আছে;
  • উদ্ভূত ডেটা আলাদাভাবে গণনা ও ক্যাশ করুন। একটি তালিকার কেবল id, title ও status দরকার হলে স্টোরে পুরো রেকর্ড রাখবেন না;
  • উচ্চ-কম্পাঙ্কের স্ট্রিং নরমালাইজ করুন। "paid" ও "pending"-এর মতো মান সংখ্যিক এনামে ম্যাপ করুন, স্ট্রিং অবজেক্ট ও তুলনার খরচ — দুটোই বাঁচে;
  • দীর্ঘজীবী রেফারেন্স কাটুন। কাজ শেষে বড় অবজেক্ট null করুন, ক্যাশে পরিষ্কার করুন, কলব্যাক আনরেজিস্টার করুন যাতে ক্লোজার সেগুলো পরের পেজে বহন না করে;
  • Worker কপি খরচ হিসাব করুন। postMessage ডিফল্টে structured clone ব্যবহার করে; ৫০MB অবজেক্ট গ্রাফ মেইন থ্রেডে ফেরত পাঠালে দ্বিতীয় কপি তৈরি হয়। সম্ভব হলে সারসংক্ষেপ ফেরত দিন।

আকার খাতার অন্য অর্ধেক আছে ভুল ক্রমে JSON কম্প্রেশন বন্ধ করুন-এ: ডিস্ক বাইট ও তারের বাইট হিপের অবজেক্ট গ্রাফ থেকে আলাদা হিসাব, আর কোনো একক শতাংশ দুইটিই ব্যাখ্যা করে না। এডিটর ও CLI টুল মেমরিতে কেমন আচরণ করে জানতে দেখুন এডিটর, কমান্ড-লাইন টুল ও অটোমেশন।

৯. চেকলিস্ট

লক্ষণ বেশি সম্ভাব্য কারণ প্রথম পদক্ষেপ
১০MB ফাইল প্রসেসকে ৫০MB-তে ঠেলে অবজেক্ট গ্রাফ ও টেক্সট — দুটোই জীবিত থাকে ধাপে ধাপে মাপুন; টেক্সট ধরে আছে কি না দেখুন
পার্সে OOM, স্থিতিশীল অবস্থায় স্বাভাবিক পার্স শিখর পুরোনো ডেটার সঙ্গে ওভারল্যাপ করে নতুন ফাইল পার্সের আগে পুরোনো অবজেক্ট ছাড়ুন
মেমরি ধীরে বাড়ে, কখনো নামে না স্লাইস, ক্যাশে বা ক্লোজার রেফারেন্স ধরে হিপ স্ন্যাপশট নিয়ে retained size অনুযায়ী সাজান
অ্যারে প্রত্যাশার চেয়ে বড় একটি দশমিক এলিমেন্ট কাইন্ড অবনত করেছে সংখ্যার টাইপ একীভূত করুন; delete এড়ান
ক্যাশে যোগ করলে আরও ধীর কি ও মান মেমরি খায়, GC চাপ বাড়ায় ক্যাশে সীমিত করুন ও কেবল সারসংক্ষেপ রাখুন

JSON.parse-এর পর মেমরি তিন বা চার গুণ বাড়ে কেন?

কারণ টেক্সট কেবল ইনপুট; পার্সের ফল হলো অবজেক্ট গ্রাফ। প্রতিটি অবজেক্টের হেডার, প্রতিটি প্রপার্টির স্লট, আর প্রতিটি স্ট্রিংয়ের নিজস্ব হেডার। যথেষ্ট নোডে মোট ওভারহেড টেক্সটের বাইট সংখ্যাকে ছাড়িয়ে যায়। গুণক ডেটার আকৃতির উপর নির্ভর করে, আর সমতল অবজেক্ট অ্যারে গভীর নেস্টেড ডকুমেন্ট থেকে অনেক আলাদা।

১০MB JSON ফাইল পূর্ণ পার্স করা উচিত নাকি স্ট্রিম?

আগে উদ্দেশ্য দেখুন। র‍্যান্ডম অ্যাক্সেস দরকার, বেশিরভাগ ফিল্ড দরকার, আর ফাইল মেমরিতে ফিট হলে পূর্ণ JSON.parse ব্যবহার করুন। কেবল কয়েকটি ফিল্ড দরকার, বা ফাইল না ধরলে স্ট্রিম করুন। ১০MB কোনো সীমানা নয়; স্ট্রিমিং সাধারণত ১০০MB-এর উপরে বাধ্যতামূলক হয়।

process.memoryUsage().heapUsed সিস্টেম মনিটরিংয়ের সঙ্গে মেলে না কেন?

heapUsed কেবল JS হিপের ব্যবহৃত অংশ ঢাকে, external, arrayBuffers ও কোড স্পেস বাদ দিয়ে। সিস্টেম মনিটরিং rss জানায়, যা ভিন্ন মাপ। heapUsed ও rss একসঙ্গে ট্র্যাক করুন; বড় ফাঁক Buffer ও নেটিভ অবজেক্টের দিকে ইশারা করে।

JSON.parse(JSON.stringify(x)) নিরাপদ গভীর ক্লোন কি?

না। এটি মূল গ্রাফ, সাময়িক টেক্সট ও নতুন গ্রাফ একসঙ্গে ধরে, তাই শিখর স্থিতিশীল অবস্থার প্রায় তিন গুণ, আর এটি undefined, ফাংশন ও Date সেমান্টিক্স ফেলে দেয়। রিড-অনলি কাজে রেফারেন্স দিন; বিচ্ছিন্নতা দরকার হলে structuredClone ব্যবহার করুন আর একই পুনর্গঠন খরচ মেনে নিন।