বড় JSON পার্সিং, রেন্ডারিং ও স্ট্রিমিং
সংক্ষেপে: পার্সিং প্রায়ই bottleneck নয়; bottleneck হলো রেন্ডারিং ও কপি। "পার্স → ট্রাভার্স → রেন্ডার → পরিবহন" — এই চার স্তরে অপ্টিমাইজ করুন যাতে পরিশ্রম বৃথা না যায়।

মূল কথা: আগে পার্সিং, ট্রাভার্সাল, রেন্ডারিং ও পরিবহন আলাদাভাবে মাপুন, তারপর এই ক্রমে কাজ করুন — কপি কমান, চাহিদামাফিক এক্সপ্যান্ড ও ভার্চুয়াল স্ক্রলিং, কাজ Worker-এ সরান, স্ট্রিমিং-এ যান, আর কেবল তারপর ফরম্যাট বদলান। স্ট্রিমিং লাভজনক কেবল যখন ফাইল মেমরিতে ধরে না বা আপনি এর ফিল্ডের ছোট উপসেট দরকার।
"JSON.parse ধীর" — এই সিদ্ধান্ত দশবারের আটবারই ভুল। আসলে যা ধীর তা হলো পার্স করার পরে আপনি যা করেন।
১. আগে মাপুন, তারপর অপ্টিমাইজ করুন
একই ১০MB JSON ফাইলে ভিন্ন ধাপের ব্যয়িত সময় মাত্রার ক্রমে আলাদা:
| ধাপ | সাধারণ সময় | প্রধান খরচ |
|---|---|---|
JSON.parse (নেটিভ) |
কয়েক ডজন থেকে একশ মিলিসেকেন্ডের সামান্য বেশি | একটি রৈখিক স্ক্যান প্লাস অবজেক্ট গঠন |
গভীর কপি (structuredClone / রিকার্সিভ) |
পার্সের সম্ভবত ১–৩ গুণ | পুরো অবজেক্ট গ্রাফ পুনর্গঠন |
| গভীর তুলনা / ডিফ | সম্ভবত আবার দ্বিগুণ | রিকার্সিভ ট্রাভার্সাল প্লাস স্ট্রিং তুলনা |
| DOM ট্রিতে রেন্ডার | কয়েক হাজার নোড হলে জমে যায় | লেআউট ও রিফ্লো |
| স্ট্রিং কনক্যাট ও তারপর সিরিয়ালাইজেশন | আউটপুট আকারের সমানুপাতিক | মেমরি দ্বিগুণ |
তাই কাজ করার আগে মাপুন: ব্রাউজারে প্রতিটি ধাপ performance.now() দিয়ে মুড়ুন বা console.time ব্যবহার করুন;
Node-এ process.memoryUsage() দিয়ে হিপ বৃদ্ধি দেখুন। বেসলাইন নেই, অপ্টিমাইজেশন নেই।
২. পার্সিং স্তর: তিন কৌশল
২.১ পূর্ণ পার্স (ডিফল্ট পছন্দ)
const data = JSON.parse(text);
নেটিভ বাস্তবায়ন স্ক্যানিং ও অবজেক্ট গঠন C++-এ করে, তাই বিশুদ্ধ JS লাইব্রেরি এটিকে খুব একটা হারাতে পারে না। নিচের তিন শর্ত পূরণ হলে এটি ব্যবহার করুন:
- টেক্সট পুরোপুরি মেমরিতে ফিট করে (সাধারণত < ১০০MB);
- পার্সের পর বেশিরভাগ ফিল্ড আসলে দরকার;
- ডেটার অবিশ্বস্ততার মাত্রা সামলানো যায়।
২.২ স্ট্রিমিং ইনক্রিমেন্টাল পার্সিং
stream-json-এর মতো SAX-শৈলীর পার্সার দিয়ে সম্পূর্ণ অবজেক্ট ট্রি না বানিয়ে লক্ষ্য ফিল্ড স্ক্যান করে বের করা যায়:
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) {
// Process one element at a time; memory is independent of element count
handleItem(value);
}
খরচ:
- কোড জটিলতা উল্লেখযোগ্যভাবে বাড়ে ও এরর হ্যান্ডলিং জটিল হয়;
- কেবল অনুক্রমিক অ্যাক্সেস সম্ভব, র্যান্ডম অ্যাক্সেস নেই;
- বৈশ্বিক অ্যাগ্রিগেশন দরকার এমন চাহিদার জন্য প্রতিকূল।
সিদ্ধান্তের নিয়ম: ফাইল ধরে না গেলে, বা এর মাত্র খুব ছোট উপসেট ফিল্ড দরকার হলে তবেই স্ট্রিমিং লাভজনক।
২.৩ ফরম্যাট বদলান
ডেটা "এক ব্যাচ রেকর্ড" হলে "একক ডকুমেন্ট" নয়, শুরু থেকেই JSON Lines ব্যবহার করা উচিত ছিল। লাইনভিত্তিক ফরম্যাট "স্ট্রিমিং"-কে "লাইন ধরে পড়া"-তে বদলায়, বাস্তবায়ন জটিলতা এক মাত্রা কমিয়ে।
৩. ট্রাভার্সাল স্তর: একটি কপি কমালেই অর্ধেক যুদ্ধ
// Anti-pattern: deep copying first "for safety"
const safeCopy = structuredClone(bigData); // Memory doubles immediately
// Good pattern: pass references directly in read-only scenarios
function renderSummary(data: OrderDocument): Summary {
return { total: data.items.length, amount: data.total };
}
সাধারণ লুকানো কপি:
JSON.parse(JSON.stringify(x))"গভীর কপি" হিসেবে — এটাই সবচেয়ে দামি ধরন;{ ...data }কেবল এক স্তর কপি করে, কিন্তু বড় অবজেক্টে সব টপ-লেভেল কি-এর রেফারেন্স অ্যারে তবু কপি হয়;Array.prototype.mapনতুন অ্যারে বানায়, যা এলিমেন্ট বেশি হলে খরচও বেশি;- ফ্রেমওয়ার্কের প্রতিক্রিয়াশীল প্রক্সি (Vue-র
reactive) বড় অবজেক্ট পুনরাবৃত্তভাবে প্রক্সি করে, আর ওভারহেড পার্সের চেয়েও বেশি হতে পারে।
ব্যবহারিক পরামর্শ: "কাঁচা ডেটা"-কে সাধারণ অপরিবর্তনীয় অবজেক্ট রাখুন ও প্রতিক্রিয়াশীলতায় মুড়বেন না; প্রদর্শনের জন্য দরকারি উদ্ভূত ডেটা আলাদাভাবে গণনা ও ক্যাশ করুন।
৪. রেন্ডারিং স্তর: পুরো ট্রি DOM-এ ঢুকবেন না
৫MB JSON ফাইলে মোটামুটি ৩০,০০০ থেকে ১,০০,০০০ নোড; সব DOM এলিমেন্ট হিসেবে রেন্ডার করলে ব্রাউজার সাড়া দেওয়া বন্ধ করে।
তিনটি কার্যকর পথ:
৪.১ চাহিদামাফিক এক্সপ্যান্ড
কেবল রুট নোডের প্রত্যক্ষ সন্তান রেন্ডার করুন, আর নোড এক্সপ্যান্ড হলে পরের স্তর গণনা করুন। এটি ট্রি ভিউয়ারের মান পদ্ধতি:
type TreeNode = { key: string; value: unknown; expanded: boolean };
/**
* Returns only the currently visible nodes, avoiding building the whole tree at once.
* @param rootNode Root node
* @param expandedPaths Set of paths that have been expanded
* @returns List of visible nodes
*/
function collectVisibleNodes(rootNode: TreeNode, expandedPaths: Set<string>): TreeNode[] {
const visibleNodes: TreeNode[] = [];
/**
* Recursively collects the child nodes of expanded branches.
* @param currentNode Current node
* @param currentPath Current path
*/
const walk = (currentNode: TreeNode, currentPath: string): void => {
visibleNodes.push(currentNode);
if (!expandedPaths.has(currentPath) || typeof currentNode.value !== 'object' || currentNode.value === null) {
return;
}
for (const [childKey, childValue] of Object.entries(currentNode.value)) {
walk({ key: childKey, value: childValue, expanded: false }, `${currentPath}/${childKey}`);
}
};
walk(rootNode, '');
return visibleNodes;
}
৪.২ ভার্চুয়াল স্ক্রলিং
এক্সপ্যান্ড করা নোড কয়েকশো ছাড়ালে কেবল ভিউপোর্টের ভেতরের কয়েক ডজন রেন্ডার করুন আর বাকির জন্য প্লেসহোল্ডার উচ্চতা দিয়ে জায়গা সংরক্ষণ করুন। লিস্ট-শৈলীর ভিউতে (টেবিল, লগ) এটি সবচেয়ে কার্যকর।
৪.৩ কেবল সারসংক্ষেপ দেখান
আগে "কাঠামো + টাইপ + গণনা" দেখান (যেমন items: Array(12000)); ব্যবহারকারী আইটেম খোললেই কেবল প্রকৃত মান আনুন।
"শুধু ফিল্ড ঠিক আছে কি না নিশ্চিত করতে চাই" পরিস্থিতিতে এই কৌশল সবচেয়ে ভালো।
৫. মেইন থ্রেড: যখন উচিত তখন কাজ Worker-এ সরান
১MB-এর বড় যেকোনো পার্সিং, ফরম্যাটিং ও ডিফ Web Worker-এ যাওয়া উচিত:
// json.worker.ts
self.onmessage = (event: MessageEvent<{ text: string }>) => {
try {
const parsed = JSON.parse(event.data.text);
self.postMessage({ ok: true, data: parsed });
} catch (parseError) {
self.postMessage({ ok: false, message: (parseError as Error).message });
}
};
/**
* Parses JSON inside a Worker so the main thread is not blocked.
* @param text JSON text
* @returns Parse result
*/
function parseInWorker(text: string): Promise<unknown> {
return new Promise((resolvePromise, rejectPromise) => {
const worker = new Worker(new URL('./json.worker.ts', import.meta.url), { type: 'module' });
worker.onmessage = (event) => {
worker.terminate();
if (event.data.ok) {
resolvePromise(event.data.data);
} else {
rejectPromise(new Error(event.data.message));
}
};
worker.postMessage({ text });
});
}
Worker-এর যোগাযোগ খরচ খেয়াল রাখুন: postMessage ডিফল্টে structured cloning ব্যবহার করে, আর ১০MB অবজেক্ট একবার ক্লোন করতেও কয়েক ডজন মিলিসেকেন্ড লাগে।
যদি কেবল "পার্স করে মেইন থ্রেডে দেওয়া" দরকার হয়, ভারী কাজ Worker-এর ভেতরেই করুন (পার্স + রূপান্তর + যাচাই) আর ফলের সারসংক্ষেপ ফেরত পাঠান।
৬. পরিবহন স্তর: আকার অপ্টিমাইজেশন
| কৌশল | সাধারণ লাভ | খরচ |
|---|---|---|
| gzip / brotli | কম্প্রেসড আউটপুট প্রায়ই মূল আকারের ১০%–২৫% | সার্ভার সমর্থন দরকার |
| ইনডেন্টেশন ও হোয়াইটস্পেস সরানো | ২০%–৪০% হ্রাস | মানুষ-পাঠযোগ্য নয় |
| ফিল্ড-নাম সংক্ষিপ্ত করা | গভীর নেস্টিংয়ে লক্ষণীয় লাভ | কম পাঠযোগ্যতা; ম্যাপিং টেবিল দরকার |
| এনাম স্ট্রিংয়ের বদলে সংখ্যা | সামান্য | অভিধান দরকার |
| পেজিনেশন / চাহিদামাফিক ফিল্ড | সবচেয়ে বেশি | API পুনঃনকশা |
মানুষের জন্য বানানো পেজে সঠিক সমন্বয়: API কম্প্যাক্ট, কম্প্রেসড JSON পাঠায়, আর UI কেবল প্রদর্শনের জন্য প্রিটি করে। অনেক টুল সাইট ডিফল্টে আবার প্রিটি টেক্সট ফ্রন্টএন্ডে পাঠায়, বিনা কারণে ৩০% বেশি বাইট পাঠিয়ে।
৭. বেঞ্চমার্ক কীভাবে করবেন
/**
* Measures the median duration of a synchronous routine so a single jittery run does not skew the result.
* @param runOnce Function under test
* @param repeatCount Number of repetitions
* @returns Median duration in milliseconds
*/
function measureMedian(runOnce: () => void, repeatCount = 9): number {
const durations: number[] = [];
for (let roundIndex = 0; roundIndex < repeatCount; roundIndex += 1) {
const startedAt = performance.now();
runOnce();
durations.push(performance.now() - startedAt);
}
durations.sort((firstValue, secondValue) => firstValue - secondValue);
return durations[Math.floor(durations.length / 2)];
}
শৃঙ্খলার তিন নিয়ম:
- নির্দিষ্ট নমুনা ব্যবহার করুন, কখনো দৈবক্রমে ডেটা জেনারেট করবেন না, নাহলে সংখ্যা তুলনাযোগ্য নয়;
- অন্তত ৯ বার চালিয়ে মধ্যমা নিন; প্রথম রানে JIT ওয়ার্ম-আপ থাকে আর তা বাদ দিতে হবে;
- রানটাইম পরিবেশ (Node সংস্করণ, CPU) রেকর্ড করুন, নাহলে ক্রস-মেশিন তুলনা অর্থহীন।
৮. চেকলিস্ট
- [ ] বেসলাইন সংখ্যা আছে যাতে আগে-পরে তুলনা করা যায়;
- [ ] ১MB-এর বড় যেকোনো পার্সিং Worker-এ চলে;
- [ ] ট্রি ভিউ চাহিদামাফিক এক্সপ্যান্ড করে আর লিস্ট ভার্চুয়াল স্ক্রলিং ব্যবহার করে;
- [ ] অর্থহীন গভীর কপি বা প্রতিক্রিয়াশীল মোড়ক নেই;
- [ ] ডেটা "এক ব্যাচ রেকর্ড" হলে JSON Lines ব্যবহৃত হয়;
- [ ] API কম্প্যাক্ট JSON পাঠায় আর ফরম্যাটিং কেবল প্রদর্শনের জন্য হয়।