JSON Toolkit

बड़ी JSON का पार्सिंग, रेंडरिंग और स्ट्रीमिंग

मुख्य बात: पार्सिंग अक्सर अड़चन नहीं होती; रेंडरिंग और कॉपी होती हैं। "पार्स → ट्रैवर्स → रेंडर → ट्रांसपोर्ट" के चार स्तरों पर अनुकूलन करें ताकि आपका प्रयास व्यर्थ न जाए।

मध्यम~13 मिनट पढ़ें2026-10-30 को प्रकाशित2026-10-31 को अपडेट किया गया
बड़ी JSON का पार्सिंग, रेंडरिंग और स्ट्रीमिंग का कवर

मुख्य निष्कर्ष: पहले पार्सिंग, ट्रैवर्सल, रेंडरिंग, और ट्रांसपोर्ट अलग-अलग मापें, फिर इस क्रम में काम करें — कॉपी कम करें, वर्चुअल स्क्रॉलिंग से माँग पर विस्तृत करें, काम को Worker में ले जाएँ, स्ट्रीमिंग पर स्विच करें, और केवल तभी फ़ॉर्मेट बदलें। स्ट्रीमिंग केवल तब लायक है जब फ़ाइल मेमोरी में फ़िट न हो या आपको उसके फ़ील्ड का छोटा उपसमूह चाहिए।

"JSON.parse धीमा है" निष्कर्ष दस में से आठ बार ग़लत है। असल में धीमा वह है जो आप पार्सिंग के बाद करते हैं।

1. पहले मापें, फिर अनुकूलित करें

उसी 10MB JSON फ़ाइल के लिए, अलग-अलग चरणों में बिताया गया समय परिमाण-कोटि से भिन्न होता है:

चरण विशिष्ट अवधि मुख्य लागत
JSON.parse (नेटिव) दसियों से सौ से थोड़ा अधिक मिलीसेकंड एक रैखिक स्कैन प्लस ऑब्जेक्ट निर्माण
डीप कॉपी (structuredClone / रिकर्सिव) संभवतः पार्स का 1–3 गुना पूरे ऑब्जेक्ट ग्राफ़ का पुनर्निर्माण
डीप तुलना / diff संभवतः फिर दोगुना रिकर्सिव ट्रैवर्सल प्लस स्ट्रिंग तुलना
DOM ट्री में रेंडरिंग दसियों हज़ार नोड्स होते ही फ़्रीज़ लेआउट और रीफ़्लो
स्ट्रिंग कन्कैटनेशन फिर सीरियलाइज़ेशन आउटपुट आकार के अनुपात में मेमोरी दोगुनी

इसलिए कार्य करने से पहले मापें: ब्राउज़र में, हर चरण को performance.now() से लपेटें, या console.time का उपयोग करें; Node में, हीप वृद्धि देखने के लिए process.memoryUsage() का उपयोग करें। बेसलाइन नहीं, अनुकूलन नहीं।

2. पार्सिंग परत: तीन रणनीतियाँ

2.1 पूरा पार्स (डिफ़ॉल्ट विकल्प)

const data = JSON.parse(text);

नेटिव कार्यान्वयन स्कैनिंग और ऑब्जेक्ट निर्माण C++ में करता है, इसलिए शुद्ध JS लाइब्रेरी शायद ही इसे हरा सके। जब तक निम्न तीन शर्तें पूरी हों, इसका उपयोग करें:

  • टेक्स्ट पूरी तरह मेमोरी में फ़िट होता है (आम तौर पर < 100MB);
  • पार्सिंग के बाद अधिकांश फ़ील्ड वास्तव में चाहिए;
  • डेटा के अविश्वसनीय होने की मात्रा प्रबंधनीय है।

2.2 स्ट्रीमिंग इंक्रीमेंटल पार्सिंग

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) {
  // एक समय में एक एलिमेंट संसाधित करें; मेमोरी एलिमेंट गिनती से स्वतंत्र है
  handleItem(value);
}

लागतें:

  • कोड जटिलता काफ़ी बढ़ती है और त्रुटि प्रबंधन अधिक उलझा हुआ हो जाता है;
  • केवल क्रमिक पहुँच संभव है, रैंडम एक्सेस नहीं;
  • वैश्विक एकत्रीकरण चाहने वाली आवश्यकताओं के लिए यह अनुकूल नहीं।

निर्णय नियम: स्ट्रीमिंग केवल तब लायक है जब फ़ाइल फ़िट होने के लिए बहुत बड़ी हो, या जब आपको उसके फ़ील्ड का बहुत छोटा उपसमूह चाहिए।

2.3 फ़ॉर्मेट बदलें

यदि डेटा "रिकॉर्ड का बैच" है, "एकल दस्तावेज़" नहीं, तो आपको शुरू से JSON Lines का उपयोग करना चाहिए था। एक पंक्ति-सीमांकित फ़ॉर्मेट "स्ट्रीमिंग" को "पंक्ति-दर-पंक्ति पढ़ना" बना देता है, जिससे कार्यान्वयन जटिलता परिमाण-कोटि कम हो जाती है।

3. ट्रैवर्सल परत: एक कम कॉपी आधी लड़ाई जीत लेती है

// विरोधी पैटर्न: "सुरक्षा के लिए" पहले डीप कॉपी करना
const safeCopy = structuredClone(bigData);   // मेमोरी तुरंत दोगुनी

// अच्छा पैटर्न: केवल-पढ़ने परिदृश्यों में सीधे संदर्भ पास करें
function renderSummary(data: OrderDocument): Summary {
  return { total: data.items.length, amount: data.total };
}

आम छिपी हुई कॉपी:

  • "डीप कॉपी" के रूप में JSON.parse(JSON.stringify(x)) — यह सबसे महँगा प्रकार है;
  • { ...data } केवल एक स्तर कॉपी करता है, लेकिन बड़े ऑब्जेक्ट के लिए यह अब भी सभी शीर्ष-स्तरीय कुंजियों का संदर्भ सरणी कॉपी करता है;
  • Array.prototype.map नया सरणी बनाता है, जो बहुत से एलिमेंट्स के साथ भी महँगा है;
  • फ़्रेमवर्क रिएक्टिविटी प्रॉक्सी (Vue का reactive) बड़े ऑब्जेक्ट को पुनरावर्ती रूप से प्रॉक्सी करते हैं, और ओवरहेड पार्सिंग से भी अधिक हो सकता है।

व्यावहारिक सलाह: "कच्चे डेटा" को एक सादे अपरिवर्तनीय ऑब्जेक्ट के रूप में रखें और उसे रिएक्टिविटी में न लपेटें; जो व्युत्पन्न डेटा आपको प्रदर्शित करना है उसकी गणना और कैश अलग से करें।

4. रेंडरिंग परत: पूरा ट्री DOM में न ठूँसें

5MB JSON फ़ाइल में लगभग 30,000 से 100,000 नोड्स होते हैं; उन सबको DOM एलिमेंट के रूप में रेंडर करने से ब्राउज़र प्रतिक्रिया देना बंद कर देगा।

तीन व्यवहार्य दृष्टिकोण:

4.1 माँग पर विस्तृत करें

केवल रूट नोड के सीधे बच्चों को रेंडर करें, और अगला स्तर तभी गणना करें जब नोड विस्तृत हो। यह ट्री व्यूअर के लिए मानक दृष्टिकोण है:

type TreeNode = { key: string; value: unknown; expanded: boolean };

/**
 * केवल वर्तमान में दृश्यमान नोड्स लौटाता है, ताकि पूरा ट्री एक साथ न बने।
 * @param rootNode रूट नोड
 * @param expandedPaths विस्तृत किए गए पथों का समूह
 * @returns दृश्यमान नोड्स की सूची
 */
function collectVisibleNodes(rootNode: TreeNode, expandedPaths: Set<string>): TreeNode[] {
  const visibleNodes: TreeNode[] = [];

  /**
   * विस्तृत शाखाओं के चाइल्ड नोड्स को पुनरावर्ती रूप से एकत्र करता है।
   * @param currentNode वर्तमान नोड
   * @param currentPath वर्तमान पथ
   */
  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;
}

4.2 वर्चुअल स्क्रॉलिंग

एक बार विस्तृत नोड्स कुछ सौ से अधिक हो जाएँ, तो केवल व्यूपोर्ट के अंदर के कुछ दर्जन रेंडर करें और बाक़ी के लिए प्लेसहोल्डर ऊँचाइयों से जगह आरक्षित करें। यह सूची-शैली व्यू (तालिकाएँ, लॉग) पर सबसे प्रभावी है।

4.3 केवल सारांश दिखाएँ

पहले "संरचना + प्रकार + गिनती" प्रस्तुत करें (उदाहरण के लिए items: Array(12000)); केवल तब ठोस मान लाएँ जब उपयोगकर्ता कोई आइटम खोले। यह तरकीब "मुझे बस पुष्टि करनी है कि फ़ील्ड सही हैं" परिदृश्य के लिए सबसे अच्छी काम करती है।

5. मुख्य थ्रेड: जब चाहिए तब काम Worker में ले जाएँ

1MB से बड़ी किसी भी चीज़ का पार्सिंग, फ़ॉर्मेटिंग, और डिफ़िंग 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 });
  }
};
/**
 * JSON को Worker के अंदर पार्स करता है ताकि मुख्य थ्रेड अवरुद्ध न हो।
 * @param text JSON टेक्स्ट
 * @returns पार्स परिणाम
 */
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 });
  });
}

Workers की संचार लागत का ध्यान रखें: postMessage डिफ़ॉल्ट रूप से structured cloning का उपयोग करता है, और 10MB ऑब्जेक्ट को एक बार क्लोन करने में भी दसियों मिलीसेकंड लगते हैं। यदि आपको केवल "इसे पार्स करके मुख्य थ्रेड को सौंपना" है, तो भारी काम Worker के अंदर करें (पार्स + रूपांतरित + सत्यापित) और परिणाम सारांश वापस भेजें।

6. ट्रांसपोर्ट परत: आकार अनुकूलन

तकनीक सामान्य लाभ लागत
gzip / brotli कम्प्रेस्ड आउटपुट अक्सर मूल आकार का 10%–25% सर्वर समर्थन चाहिए
इंडेंटेशन और व्हाइटस्पेस हटाना 20%–40% कमी मानव-पठनीय नहीं
फ़ील्ड नाम संक्षिप्त करना गहरी नेस्टिंग के साथ ध्यान देने योग्य लाभ कम पठनीयता; मैपिंग तालिका चाहिए
enum स्ट्रिंग्स के बजाय संख्याएँ छोटा शब्दकोश चाहिए
पेजिनेशन / माँग पर फ़ील्ड सबसे बड़ा API पुनः डिज़ाइन

मनुष्यों के लिए बने पेजों के लिए, सही संयोजन है: API कॉम्पैक्ट, कम्प्रेस्ड JSON भेजे, और UI उसे केवल प्रदर्शन के लिए सुंदर बनाए। बहुत सी टूल साइटें डिफ़ॉल्ट रूप से सुंदर टेक्स्ट फिर से फ्रंटएंड को भेजती हैं, बिना किसी कारण 30% अधिक बाइट्स भेजती हैं।

7. बेंचमार्क कैसे करें

/**
 * किसी सिंक्रोनस रूटीन की माध्यिका अवधि मापता है ताकि एक काँपता रन परिणाम को विकृत न करे।
 * @param runOnce परीक्षण के अधीन फ़ंक्शन
 * @param repeatCount पुनरावृत्तियों की संख्या
 * @returns मिलीसेकंड में माध्यिका अवधि
 */
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)];
}

अनुशासन के तीन नियम:

  1. निश्चित नमूना उपयोग करें और कभी यादृच्छिक रूप से डेटा न उत्पन्न करें, अन्यथा संख्याएँ तुलनीय नहीं होंगी;
  2. कम से कम 9 बार चलाएँ और माध्यिका लें; पहला रन JIT वार्म-अप शामिल करता है और उसे त्यागना चाहिए;
  3. रनटाइम वातावरण दर्ज करें (Node संस्करण, CPU), अन्यथा क्रॉस-मशीन तुलना निरर्थक है।

8. चेकलिस्ट

  • [ ] बेसलाइन संख्याएँ मौजूद हैं ताकि पहले और बाद की तुलना हो सके;
  • [ ] 1MB से बड़ी किसी भी चीज़ का पार्सिंग Worker में चलता है;
  • [ ] ट्री व्यू माँग पर विस्तृत होते हैं और सूचियाँ वर्चुअल स्क्रॉलिंग का उपयोग करती हैं;
  • [ ] कोई निरर्थक डीप कॉपी या रिएक्टिव रैपिंग नहीं;
  • [ ] जब डेटा "रिकॉर्ड का बैच" हो तो JSON Lines का उपयोग होता है;
  • [ ] API कॉम्पैक्ट JSON भेजती है और फ़ॉर्मेटिंग केवल प्रदर्शन के लिए होती है।