• MongoDB और PostgreSQL के लिए database GUI बनाते समय BSON/JSONB types, nested columns, search, editing, pinning और drag को support करना था; इसके लिए करीब 1 साल तक दो-अक्ष virtualisation और state management architecture को optimize किया गया
  • मूल documents से अलग display string, type, flattened path, column order और search results को पहले से calculate करने वाली shadow table बनाई गई, और screen पर दिख रही rows और columns को ही fixed-size DOM में render किया गया
  • Scroll path में passive event listeners, requestAnimationFrame, buffers/hysteresis और velocity tracking लागू किए गए, और layout properties की जगह transform और opacity का इस्तेमाल कर main thread का काम घटाया गया
  • प्रति-cell icons को shared SVG background images से बदला गया और editor को केवल जरूरत पड़ने पर mount किया गया; rows और columns को position के आधार पर track करने वाली DOM pooling से scrolling के दौरान node creation हटाया गया
  • Canvas DOM की तुलना में 60fps की ऊंची performance ceiling देता है, लेकिन text, selection, accessibility और feature expansion में कमजोर है; इसलिए real text selection और तेज development बनाए रखने के लिए DOM-based design चुना गया

लक्ष्य और शुरुआती सीमाएं

  • शुरुआत एक simple 2D array और nested loops से हुई, लेकिन यह लगभग 1 साल तक बीच-बीच में चलने वाले optimization work में बदल गया
  • Database GUI की table सिर्फ display के लिए नहीं थी; उसे कई तरह की states और interactions support करनी थीं
    • MongoDB के सभी BSON types और PostgreSQL आदि के JSONB को समझना और type-specific color icons दिखाना
    • string "123" और integer 123 जैसे cases में, जहां query result बदल जाता है, types में फर्क करना
    • nested documents को असली sub-columns में expand करना, और पूरे nested path में search करके cell के अंदर matching हिस्सों को highlight करना
    • column reorder/resize/pin, cell के अंदर editing, और values/rows/columns को visual query builder में drag करने की जरूरत थी
  • इन features की state मूल document में नहीं होती और scroll के बाद भी बनी रहनी चाहिए, इसलिए अलग rendering structure की जरूरत थी

चरण 1: हर item को सीधे render करना

  • rows और fields पर nested iteration करके सभी cells बनाने का तरीका 100 rows पर काम करता है, लेकिन बड़े data पर टूट जाता है
    • 10,000 rows × 30 columns करीब 300,000 DOM nodes बनाते हैं, और framework का change detection इन्हें बार-बार traverse करता है
    • अगर एक DOM node browser की internal structures सहित करीब 1KB लेता है, तो असली data से पहले ही सैकड़ों MB memory चाहिए
    • 60fps का frame budget 16.7ms है, और style/layout/paint भी यही समय साझा करते हैं
  • वास्तविक implementation में करीब 20 columns वाली 1,000 rows render करने की कोशिश असफल रही
  • केवल कुछ हिस्सा render करने के लिए current visible rows और columns, order, nested field expansion और search results को अलग से track करना पड़ता है

चरण 2: Shadow table से display state अलग करना

  • मूल document nested होते हैं और उनके types अलग-अलग होते हैं, इसलिए उन्हें rendering input की तरह इस्तेमाल करना मुश्किल है
    • MongoDB में ObjectId, Decimal128, timestamp और binary जैसे BSON values होते हैं
    • SQL data में JSONB और timezone वाले timestamps होते हैं, और हर cell के लिए format तय करना पड़ता है
  • Rendering loop में format तय करने पर हर frame वही cost दोहराई जाती है, और मूल data में column order, expansion state, search results जैसी table state भी नहीं होती
  • Shadow table मूल document बदले बिना उस state का reference point बनती है जिसे असली table दिखाएगी
    • Load के समय एक बार build होती है और state बदलने पर update होती है; scrolling के दौरान नहीं बदलती
    • हर cell के लिए truncated display string, finalized type और flattened path पहले से calculate होते हैं
    • Display string को limit किया जाता है ताकि 16MB document rendering state में 16MB string वैसी ही create न करे
    • Type icon, editor और search method तय करता है
    • "address.geo.lat" जैसे flattened path को key की तरह इस्तेमाल किया जाता है, ताकि हर बार tree traverse न करना पड़े
  • Nested object expand करने पर sub-paths असली columns में promote हो जाते हैं, और sort/search results/column order/expansion state भी उसी structure में store होते हैं
  • इस चरण में DOM count कम नहीं होता, लेकिन आगे column order और width को तेजी से calculate करने की नींव तैयार होती है

चरण 3: Vertical virtualisation और ghost scroll area

  • Vertical virtualisation viewport की rows और थोड़ा buffer ही render करता है, बाकी height को fake area बना देता है
  • Ghost area (phantom) rowCount × rowHeight ऊंचाई वाला internal container है
    • 10 लाख rows × 40px होने पर लगभग खाली content वाला 4 करोड़ px ऊंचा div बनता है
    • Browser इसी height के आधार पर native scrollbar और scroll behavior देता है
  • Display range निम्न calculation से मिलती है
    • firstRow = floor(scrollTop / rowHeight)
    • lastRow = floor((scrollTop + viewportHeight) / rowHeight)
    • छोटी-छोटी movements पर बार-बार render न हो, इसलिए दोनों तरफ buffer rows जोड़ी जाती हैं
  • Display rows को firstRow × rowHeight position वाले slab container में रखा जाता है, और top की बजाय transform इस्तेमाल करना बेहतर है
  • User को 10 लाख rows दिखती हैं, लेकिन DOM में लगभग 40 rows ही मौजूद होती हैं
  • लेकिन अगर 300 columns हों, तो सिर्फ 40 rows से भी 12,000 cells बनते हैं, इसलिए columns को भी virtualise करना पड़ता है

चरण 4: Variable width संभालने वाली horizontal virtualisation

  • Document database collections में सैकड़ों fields हो सकते हैं, इसलिए column virtualisation भी जरूरी है
  • Column widths fixed नहीं हैं, इसलिए fixed-value division की जगह cumulative sums और binary search इस्तेमाल किया जाता है
    • position[n] = width[0] + ... + width[n-1] जैसे cumulative sum से हर column का x coordinate एक array lookup में मिल जाता है
    • Scroll offset x से संबंधित column cumulative sum array पर binary search करके मिलता है
    • 1,000 columns हों तब भी search microseconds में खत्म हो जाता है
  • Cumulative sum केवल column resize/hide/reorder जैसे cases में rebuild होता है, जब width सच में बदलती है; scrolling के दौरान नहीं बनता
  • बाएं-दाएं करीब 200px buffer रखा जाता है, ताकि अगला column screen पर आने से पहले render हो जाए
  • दो-अक्ष virtualisation के बाद rendering area data size से स्वतंत्र होकर लगभग 40 rows × 12 columns बना रहता है
    • 500-column collection में भी एक बार में करीब 12 columns ही मौजूद होते हैं, इसलिए load time नहीं बढ़ा
  • Render targets कम हो गए, लेकिन प्रति second सैकड़ों बार आने वाले scroll events पर range को बार-बार calculate करने की समस्या बची रही

चरण 5: Scroll path का frame budget संभालना

  • Scroll handling style/layout/paint के साथ 16.7ms frame budget साझा करता है, इसलिए केवल न्यूनतम काम करना चाहिए
  • Passive event listeners को framework change detection के बाहर register किया गया
    • Browser को बताया जाता है कि preventDefault call नहीं होगा, ताकि compositor JavaScript का इंतजार किए बिना pixels move कर सके
    • Scroll event खुद framework के rendering checks trigger नहीं करता
  • कई events को प्रति frame एक बार में merge किया गया
    • सिर्फ latest scroll position record की जाती है और एक requestAnimationFrame callback schedule किया जाता है
    • एक frame में आए 12 events भी एक ही range calculation से handle होते हैं
  • Handsontable source से मिली fast draw exit लागू की गई
    • अगर नई display range मौजूदा rendered buffer के अंदर है, तो सिर्फ दो integer comparisons करके तुरंत return किया जाता है
  • Buffer boundaries पर hysteresis लागू किया गया
    • Display range जब buffer edge के करीब 40px के अंदर पहुंचती है, तब rebuild किया जाता है
    • करीब 200px buffer को नई position के center में फिर रखा जाता है, ताकि empty edges न दिखें और boundary पर repeated rebuild न हो
  • Events के बीच px/ms track करने वाला velocity detector जोड़ा गया
    • Implementation में 10px/ms से ऊपर होने पर इसे fast flick मानकर rendering रोक दी जाती है
    • Native scroll को ghost area पर चलने दिया जाता है और velocity stabilize होने पर slab फिर भरा जाता है
  • JavaScript work घटाने के बाद भी frame drops बचे रहे; style, zebra striping और icons सहित layout और paint अगला bottleneck बने

चरण 6: Layout property cost हटाना

  • Browser का main thread style/layout/paint संभालता है, और compositor पहले से drawn layers को GPU पर move करता है
  • जिन properties की animation compositor पर संभाली जा सकती है वे transform और opacity हैं; top, left, width, height, background-color आदि main thread को जगाते हैं
  • Row numbers और pinned column panel की scroll synchronization में top update करने पर प्रति second 60 forced layouts हुए
    • इसे translate3d से बदलकर वही screen बनाए रखते हुए main thread cost हटाई गई
  • Row-wise backgrounds से बनी zebra striping को पूरे body के एक repeating-linear-gradient से बदला गया
    • Row height CSS variable से दी गई
    • Browser सिर्फ दो-row size का एक tile rasterize करता है और GPU texture से repeated copy करता है
    • Row-level class binding हट गई और body व pinned panel एक ही tile साझा करने लगे, जिससे color mismatch भी बचा
  • Column dividers को 4 करोड़ px ऊंचे पूरे ghost area पर नहीं, बल्कि केवल currently rendered slab की height पर draw किया गया
  • Normal scroll smooth हो गया, लेकिन नए cells बनाने वाले window switch के समय हर cell में अनावश्यक DOM जमा होने की cost बची रही

चरण 7: Icons और editors को lightweight बनाना

  • Database grid के type icons सिर्फ सजावट नहीं, बल्कि ObjectId, string, integer, JSONB जैसे values का अर्थ अलग करने वाला feature हैं
  • पहली implementation में हर cell में font icon element जोड़ा गया, लेकिन extra DOM nodes और glyph text rendering path ने बड़ी cost पैदा की
  • Icons को cell के अपने background-image में move किया गया और SVG data URI के रूप में encode किया गया
    • एक ही type के सभी cells वही URI string reference करते हैं
    • Browser type-wise icon को एक बार rasterize करता है और cached GPU texture से repeated copy करता है
    • अलग DOM node के बिना repeated decoration दिखाया जा सकता है
  • Cell editing के लिए mixed rendering approach अपनाई गई
    • सामान्य स्थिति में cell सिर्फ plain text और एक span इस्तेमाल करता है
    • Double-click पर ही heavy type-aware editor component को उस cell के ऊपर portal की तरह mount किया जाता है
  • सभी cells में framework component इस्तेमाल करने पर instance creation cost जमा होती है, और grid library को cell renderer components से जोड़ने पर performance गिर सकती है
  • Cell खुद हल्का हो गया, लेकिन नया column दिखने पर framework बाकी same-content cells भी फिर से बना देता था—यह समस्या बची रही

चरण 8: Position-based DOM reuse

  • Angular का trackBy, React और Vue का key जैसे tracking criteria यह तय करते हैं कि window move होने पर existing elements reuse होंगे या destroy करके फिर बनाए जाएंगे
  • अगर row नए data से re-bind नहीं होती और हर बार replace होती है, तो components, DOM nodes और event listeners लगातार फिर से बनाने पड़ते हैं
  • Rows और columns को data value की बजाय screen position से track किया गया
    • करीब वही 40 row elements और column elements बने रहते हैं, केवल content नए values से replace होता है
    • Grid object pool की तरह काम करता है और scrolling के दौरान नया DOM allocate नहीं करता
  • इस चरण में दो-अक्ष scrolling और window rebuild cost हल हो गई, लेकिन column drop पर एक frame का jitter या row number में half-pixel error जैसी finishing issues बचीं

चरण 9: Interaction details को polish करना

  • Column drag के दौरान existing columns को transform से नए order की तरह move किया जाता है, और drop पर reorder और transform reset को उसी rendering pass में process किया जाता है
    • आखिरी drag frame और पहले reordered frame को pixel-level पर identical बनाया जाता है ताकि transition दिखाई न दे
  • Tooltip हर cell पर bind नहीं किया गया; container के एक delegated hover listener से handle किया गया
    • Tooltip केवल cursor के नीचे cell के लिए calculate होता है
    • SQL के JSONB value को pretty-print करने जैसी महंगी string generation भी actual hover time तक टाल दी जाती है
  • Row number column की 1px border data rows और text baseline को half-pixel misalign कर रही थी, जिससे scrolling में jitter बनता था
    • दोनों columns को एक ही box model इस्तेमाल करने के लिए ठीक किया गया
  • Zebra striping reusable DOM position के बजाय absolute row index के आधार पर तय की गई, ताकि virtualisation window बदलने पर color flicker न करे
  • Custom grid बनाने को justify करने वाले features भी बनाए रखे गए
    • Nested documents को JSON string की तरह छोड़ने की बजाय असली sub-columns में expand किया गया
    • Nested path में matching strings को cell के अंदर highlight किया गया
    • Typed values को grid से सीधे visual query builder में drag किया गया
  • सामान्य grid library में ऐसे features जोड़ने की cost renderer को खुद own करने की cost से ज्यादा थी

चरण 10: मौजूदा high-performance grids का source पढ़ना

  • Frontend performance सीखने में दूसरे projects का source code पढ़ना सबसे valuable habit थी, और undocumented optimizations public repositories में मौजूद थे
  • Study किए गए DOM grid AG-Grid में ये techniques लागू थीं
    • Explicit per-frame time budget और priority work queue से DOM work को time-split करना
    • Column viewport को hash करके unchanged scroll को सिर्फ एक string comparison में खत्म करना
    • Scroll direction के साथ rows create करना ताकि user जिस ओर जा रहा है, उस तरफ का content पहले दिखे
    • नए cells पहले create करना और पुराने cells destroy करने को बाद में टालना, ताकि नया content गायब हुए content से पहले draw हो
    • Per-cell event listeners measured bottleneck थे, इसलिए container-level event delegation इस्तेमाल किया गया
  • Canvas-based grids DOM को bypass करके layout और style recalculation के बिना हर frame सैकड़ों text elements फिर से draw करते हैं
    • Extreme manipulation में भी 60fps बनाए रख सकते हैं, इसलिए उनकी performance ceiling DOM grids से ऊंची है
    • Pixel grid से बाहर rasterized text धुंधला हो सकता है
    • Selection सिर्फ cell level पर काम करता है, और ellipsis भी खुद न draw करें तो दिखाई नहीं देता
    • हर नए cell feature के लिए drawing code और hit-testing code जोड़ना पड़ता है
  • अंत में DOM चुना गया, ताकि sharp text, real text selection, accessibility और fast feature development बने रहें
  • भले ही Canvas जैसी smoothness न मिले, यह जरूरी user experience और development cost को ध्यान में रखकर किया गया explicit trade-off है

अभी कोई टिप्पणी नहीं है.

अभी कोई टिप्पणी नहीं है.