Figma की TypeScript में बदलाव की यात्रा
(figma.com)- Figma ने मोबाइल rendering और prototype viewer में इस्तेमाल होने वाली अपनी भाषा Skew को पूरी तरह TypeScript में migrate किया, क्योंकि onboarding, integration और ecosystem के लिहाज से उसकी सीमाएँ सामने आने लगी थीं
- इस बदलाव की बुनियाद मोबाइल browser में WebAssembly support के बढ़ने, critical paths में C++ engine replacement, और prototyping व mobile teams की growth से बनी
- migration
Write Skew, build Skew→Write Skew, build TypeScript→Write TypeScript, build TypeScriptके 3 phases में हुआ, और Skew-to-TypeScript transpiler ने development flow बनाए रखा - असली migration के दौरान array destructuring performance, virtual call elimination (devirtualization) में फर्क, initialization order, sourcemap linking, और conditional compilation की कमी जैसी compatibility समस्याएँ सामने आईं
- अंत में Figma ने unit tests पास करने वाला और Skew जैसी performance देने वाला TypeScript codebase हासिल किया; आगे Figma internal/external code integration, package management और TypeScript ecosystem का उपयोग अगले काम के रूप में देख रहा है
Skew से TypeScript में जाने की वजह
- Skew Figma के शुरुआती side project से शुरू हुआ था, और web व mobile दोनों को support करने वाला prototype viewer तेजी से बनाने की जरूरत पूरी करता था
- JavaScript में compile होने वाली भाषा के रूप में विकसित होते हुए इसने advanced optimizations और fast compile times दिए, लेकिन prototype viewer में code जमा होने के साथ maintenance cost बढ़ती गई
- नए hires के लिए सीखना मुश्किल
- बाकी codebase के साथ आसानी से integrate नहीं होता
- Figma के बाहर कोई developer ecosystem नहीं
- TypeScript में बदलाव से expected benefits development speed और collaboration scope बढ़ाना था
- static imports और native package management से internal/external code integration सरल
- linters, bundlers, static analyzers जैसे बड़े developer community द्वारा बनाए गए tools का उपयोग
- async/await जैसे modern JavaScript features और अधिक flexible type system का उपयोग
- नए developers की onboarding और दूसरी teams की participation barrier कम करना
यह बदलाव हाल में ही संभव क्यों हुआ
- जब Figma ने पहली बार mobile codebase बनाया था, तब mobile browsers WebAssembly support नहीं करते थे, और बड़े bundles को अच्छी performance के साथ load करना भी कठिन था
- TypeScript भी शुरुआती stage में था, इसलिए static typing और ज्यादा strict type system वाला Skew उस समय अधिक practical option था
- 2018 तक WebAssembly को mobile पर व्यापक support मिल गया, और Figma tests के हिसाब से 2020 में mobile performance भरोसेमंद स्तर पर पहुँच गई
- Skew की शुरुआती strengths traditional compiler optimizations जैसे constant folding, virtual call elimination, और real integer operations इस्तेमाल करने वाला JavaScript generation जैसे web-specific optimizations थीं
- 2020 benchmark में पाया गया कि Safari में TypeScript से Figma prototype load करने पर यह लगभग 2 गुना धीमा हो सकता है, और iOS पर Safari ही allowed browser engine होने की बात migration रोकने वाला factor थी
- iOS 17.4 में Apple ने EU users के लिए दूसरे browser engines खोले, लेकिन बाकी regions के users के लिए WebKit अब भी अकेला browser engine है
- इसके बाद Skew engine के कई core components C++ engine के corresponding components से replace हो गए, जिससे TypeScript migration का performance risk घट गया
- इसमें file loading जैसे सबसे hot code paths शामिल थे
- TypeScript में migrate करने पर भी performance loss संभाला जा सकता है, इस पर confidence मिला
- prototyping और mobile teams के बड़े organization में बदलने के साथ automated migration और developer experience improvements में resources लगाने की क्षमता आई
3-phase codebase transition
- 2020 के पहले migration prototype में TypeScript performance लगभग 2 गुना धीमी थी
- WebAssembly support और mobile engine का C++ transition पर्याप्त हो जाने के बाद, Figma ने Maker Week के दौरान पुराने prototype को ठीक करके सभी tests पास करने वाला working migration demo किया
- लक्ष्य पूरे codebase को TypeScript में बदलना था, लेकिन manual rewrite development speed घटाने, runtime errors और performance regression का risk बना सकता था
- Skew और TypeScript में फर्क साधारण “typed JavaScript” परिवारों के बीच migration से काफी ज्यादा था
- TypeScript में file import होने के बाद ही namespace और class initialize होते हैं
- import order expected न हो तो runtime error आ सकता है
- Skew loading के समय सभी symbols को पूरे codebase में runtime पर उपलब्ध करा देता है
- Figma ने Evan Wallace द्वारा पहले शुरू किए गए काम के आधार पर Skew-to-TypeScript transpiler develop किया
-
Phase 1: Skew लिखना और Skew build करना
- मौजूदा build process बनाए रखते हुए transpiler develop किया गया
- generated TypeScript code को GitHub में check in किया गया ताकि developers नए codebase की shape देख सकें
-
Phase 2: Skew लिखना और TypeScript build करना
- सभी unit tests पास करने वाला TypeScript bundle बन जाने के बाद, production traffic को TypeScript codebase build पर gradually deploy किया गया
- developers अब भी Skew लिख रहे थे, और transpiler Skew code को TypeScript में convert करके GitHub का TypeScript code update करता था
- generated code के type errors भी लगातार fix किए गए
- TypeScript type errors होने पर भी valid bundle generate कर सकता था
-
Phase 3: TypeScript लिखना और TypeScript build करना
- सभी users TypeScript build process से गुजरने के बाद, TypeScript code को development का single source of truth बनाया गया
- code merge न होने का समय खोजकर auto-generation process रोका गया, और codebase से Skew code हटाया गया
- शुक्रवार रात auto-generation हटाने और CI jobs में TypeScript files को directly run कराने वाला बदलाव merge किया गया
- gradual deployment के दौरान Smart Animate feature में breakage internally मिला, और gated deployment की वजह से rollout बंद करके fix करने और plan फिर से review करने का मौका मिला
Transpiler में सामने आई समस्याएँ
- compiler आम तौर पर frontend और backend से बना होता है
- frontend input code parse करता है, type check और syntax check करता है, फिर intermediate representation (IR) में convert करता है
- backend IR को दूसरी language में convert करता है
- Skew compiler का backend obfuscated और minified JavaScript generate करता है
- transpiler ऐसा special compiler है जिसका backend human-readable code generate करता है, और Figma को Skew IR से human-readable TypeScript generate करना था
- initial implementation Skew के JavaScript backend से काफी inspired था और अपेक्षाकृत आसान रहा, लेकिन बाद में कई ऐसी समस्याएँ आईं जिन्हें track और handle करना कठिन था
-
Array destructuring performance
- sample prototype में Skew और TypeScript की offline performance difference investigate करते समय TypeScript का frame rate कम निकला
- वजह JavaScript की array destructuring थी
const [a, b] = function_that_returns_an_array()जैसे operation में JavaScript array को सीधे index नहीं करता, बल्कि iterator बनाकर iterate करता है- Figma ने JavaScript के
argumentskeyword से arguments लेने के लिए यह तरीका इस्तेमाल किया था, और कुछ test cases में performance धीमी हुई - destructuring के बजाय arguments array को direct index करने वाला code generate करके प्रति frame latency में अधिकतम 25% सुधार किया गया
-
Skew का virtual call elimination optimization
- Skew compiler कुछ conditions में class के अंदर के functions को बाहर निकालकर global functions में lift करने वाला devirtualization optimization करता है
myObject.myFunc(a, b)myFunc(myObject, a, b)के रूप में बदल सकता है- TypeScript यह optimization नहीं करता
- Smart Animate breakage तब हुआ जब
myObjectnull था - devirtualized call ठीक से चला, लेकिन non-devirtualized call ने null access exception फेंका
- Figma ने same issue वाले call sites ढूँढने के लिए devirtualization में शामिल हो सकने वाले सभी functions में logging जोड़ी
- थोड़े समय तक logging चालू रखने के बाद production logs analyze किए गए और problematic call sites fix किए गए
-
Initialization order differences
- Skew में variables, classes, namespaces और function definitions को code में कहीं भी declare करें, declaration order की चिंता नहीं होती
- TypeScript में global variables या class definitions का initialization order महत्वपूर्ण है
- class definition से पहले static class variable initialize करने पर compile-time error होता है
- शुरुआती transpiler namespace इस्तेमाल नहीं करता था और सभी functions को global scope में flatten करके Skew जैसा behavior बनाए रखता था
- resulting code पढ़ना मुश्किल था, और बाद में transpiler को ठीक किया गया ताकि TypeScript code सही order में output हो और readability के लिए namespace वापस जोड़े जाएँ
- अंत में ऐसा transpiler बना जो unit tests पास करता था और Skew performance से match करने वाला TypeScript code compile कर सकता था
- कुछ छोटी समस्याओं के लिए transpiler में नया fix डालने के बजाय Skew source code में manual fix किया गया या TypeScript transition के बाद fix किया गया
Sourcemaps से debugging experience बनाए रखना
- Figma ने TypeScript transition के दौरान भी developers बिना रुकावट debug कर सकें, इसके लिए sourcemaps को महत्वपूर्ण माना
- browser debugger केवल JavaScript समझता है, लेकिन developers Skew या TypeScript source में breakpoints set करते हैं
- sourcemaps compiled JavaScript location और original source location को connect करते हैं
- उदाहरण के तौर पर
helper → c,myInt → a,arrayOfInts → bजैसी mapping संभव है
- उदाहरण के तौर पर
- आम तौर पर
.mapextension वाली एक sourcemap file final JavaScript bundle से linked होती है - existing infrastructure Skew → JavaScript sourcemap generate करता था, लेकिन Phase 2 में pipeline Skew → TypeScript → esbuild bundling flow में बदल गई
- पुराने sourcemap को उसी तरह इस्तेमाल करने पर JavaScript और Skew code के बीच mapping गलत हो जाती, और developers debug नहीं कर पाते
- नए build process ने 3 steps में sourcemap compose किया
- Step 1: esbuild TypeScript → JavaScript sourcemap
ts-to-js.mapgenerate करता है - Step 2: transpiler हर Skew file के लिए Skew → TypeScript sourcemap generate करता है
- Step 3: दोनों sourcemaps को compose करके final Skew → JavaScript sourcemap generate किया जाता है
- Step 1: esbuild TypeScript → JavaScript sourcemap
- final sourcemap से नए JavaScript bundle को Skew code पर map किया जा सका, जिससे Phase 2 में भी developer experience बना रहा
TypeScript में conditional compilation संभालना
- Skew top-level
ifstatements और compiler को दिए जाने वालेdefinesoption से conditional compilation support करता है - इस feature से एक ही codebase से अलग-अलग bundles बनाए जा सकते थे
- users को deploy होने वाला actual bundle
- सिर्फ unit tests के लिए bundle
- debug या release build में अलग implementation इस्तेमाल करने वाले functions या classes
- TypeScript में conditional compilation नहीं है, इसलिए Figma ने type checking के बाद esbuild के
definesऔर dead code elimination features का उपयोग करके bundling stage में conditional compilation किया - इस approach में
definestype checking को प्रभावित नहीं कर सकताBUILD == "TEST"होने पर ही मौजूदtestOnlyFunctionजैसा code TypeScript में वैसे ही exist नहीं कर सकता
- समाधान यह था कि class और method हमेशा define किए जाएँ, और method के अंदर
BUILDvalue के आधार पर branch किया जाए- test-only न होने वाले build में
testOnlyFunctioncall होने परUnexpected call to test-only functionerror throw करता है
- test-only न होने वाले build में
- final JavaScript वही compile हो सकता था जैसा original Skew code generate करता था
- हालांकि कुछ symbols पहले सिर्फ specific compile mode में मौजूद थे, लेकिन बदलाव के बाद सभी modes में मौजूद रहने लगे, जिससे final bundle थोड़ा बड़ा हो गया
- tests में bundle size increase acceptable था, और export न किए गए top-level symbols tree-shaking से remove किए जा सकते थे
Transition के बाद की दिशा
- सभी Skew code को TypeScript में migrate करके Figma के एक core codebase को modernize किया गया
- internal/external code के साथ integration आसान हुआ, और developers अधिक efficiently काम कर सके
- उस समय Figma की जरूरतों और capabilities के हिसाब से Skew से codebase शुरू करने का decision सही था
- technology mature होने के साथ, TypeScript जो पहले उपयुक्त नहीं था, अब उपयुक्त option बन गया
- Figma आगे के काम में इन possibilities को explore कर रहा है
- बाकी codebase के साथ integration
- कहीं आसान package management
- active TypeScript ecosystem के नए features का direct use
- transition के दौरान import resolution, module systems और JavaScript code generation जैसे TypeScript के कई पहलू सीखे गए
1 टिप्पणियां
Hacker News की रायें
यह भी हैरानी की बात है कि Figma के पास JS के लिए custom language थी, और उससे भी ज्यादा हैरानी की बात कि वह TS से तेज थी
लेकिन आखिर में उनका ज्यादा धीमे TS पर चले जाना भी दिलचस्प है
ऐसी चीजें काफी बार दिखती हैं: कंपनी शुरुआत में अपनी तकनीक बनाती है और बड़ी होने के बाद किसी ज्यादा “standard” चीज पर migrate कर जाती है
जब team और product बहुत बड़े हो जाते हैं, तो वे फिर से किसी ज्यादा परिचित और व्यापक रूप से इस्तेमाल होने वाले stack पर platform shift करते हैं
ऐसी transition की सफलता काफी हद तक इस पर निर्भर करती है कि organization की engineering culture कितनी मजबूत है। मेरे पास सबूत नहीं है, लेकिन लगता है कि Evan और founders ने Figma में शानदार engineering culture बनाया था, और गलतियां हों तो भी उन्हें ठीक करने की पर्याप्त resilience दिखती है
उस समय तक बस glue code बचा था, और custom language के मौजूद रहने की वजह खत्म हो गई थी
फिर जब community catch up कर लेती है और company से भी बड़ा momentum हासिल कर लेती है, तो standard implementation पर जाना तार्किक हो जाता है
Google के Borg से k8s तक के flow को भी मैं थोड़ा इसी तरह देखता हूं। हालांकि Google ने अपने बनाए standard के आसपास community को invite किया था
किसी पूरी तरह नए framework को पहली बार देखकर सीख सकने वाले developer की cost की तुलना उस developer की value से करनी होती है जिसके पास पहले से इस्तेमाल होने वाले platform या technology का documented experience हो
पूरी तरह in-house development में जाएं तो किसी भी language या framework से जबरदस्त performance निकाली जा सकती है, लेकिन project को जितनी ज्यादा स्थितियां handle करनी पड़ती हैं, maintenance के लिए उतने ज्यादा developers चाहिए होते हैं, और आखिर में एक ऐसा point आता है जहां off-the-shelf software बेहतर हो जाता है, भले ही वह धीमा हो और requirements से बिल्कुल match न करता हो
अगर long term में unicorn-level hiring bar बनाए रख सकते हैं, तो fully custom stack भी maintain किया जा सकता है। लेकिन ज्यादातर organizations को उस दिशा में जाना पड़ता है जिसे average new hire आगे maintain कर सके, और आम तौर पर इसका मतलब मशहूर और boring software vendor products होता है
ambitious लोगों के लिए यह छोड़कर जाने की वजह बन सकती है, hiring मुश्किल होती है और onboarding लंबी हो जाती है। बाद में custom language छोड़ते समय बड़ा transition project भी चाहिए होता है
अगर performance gain बहुत बड़ा हो, ज्यादा safe code लिखा जा सके, या इरादा केवल ऐसे लोगों को hire करने का हो जिन्हें नई language सीखना पसंद हो, तो कभी-कभी यह worth it हो सकता है
मैंने इस project पर काम किया था। Twitter पर थोड़ा और लिखा है: https://twitter.com/andrew_k_chan/status/1786769203912925477
article का title misleading है। Figma में codebase के दूसरे हिस्सों में करीब 10 साल तक Typescript इस्तेमाल हुआ, और उस अवधि के लगभग पूरे समय Skew से ज्यादा Typescript code था
जैसा blog post में बताया गया है, Skew mobile engine, बाद में prototyping player, mirroring feature, और याद न आने वाले एक-दो product areas में इस्तेमाल हुआ था
Skew सिर्फ TypeScript से थोड़ा तेज नहीं था
Figma के पूर्व CTO Evan Wallace के मुताबिक, ज्यादा strict type system से संभव हुई optimizations की वजह से यह 1.5~2 गुना तेज था
लगता है कुछ काफी बड़ी कठिनाइयां थीं, लेकिन ज्यादा predictable optimizations के लिए dynamic behavior को थोड़ा घटाना production-grade apps के लिए अच्छा trade-off लगता है
hot execution paths को optimize करने पर 2x फायदा काफी हद तक गायब हो सकता है
बेशक तब कई जगह optimized JS जबरन लिखना पड़ेगा और readability पर असर पड़ेगा—यह बात सही है। लेकिन उसका alternative कम tool support और कम पहचान वाली पूरी तरह नई JS compile language इस्तेमाल करना था
आज देखने पर लगता है कि यह उतना worth नहीं था, और blog post ऐसा लगता है जैसे वह पहले के controversial technology choice को थोड़ा बेहतर तरीके से पेश कर रहा हो
सोच रहा हूं कि क्या TypeScript usage को उस subset तक सीमित किया जा सकता है जो यह performance देता है
const [a, b] = function_that_returns_an_array()जैसे काम को पूरा करते समय यह दिलचस्प है कि JavaScript array से सीधे indexing नहीं करती, बल्कि array को traverse करने वाला iterator बनाती हैJS destructuring assignment में array को सीधे index क्यों नहीं करती?
Symbol.iteratorजोड़ दें तो वह iterable object बन सकता है, और destructuring को ऐसे objects पर भी काम करना चाहिए. यहां तक कि array के अपनेSymbol.iteratorको भी patch किया जा सकता है, इसलिए VM को इसे handle करना पड़ता हैArray.prototype[Symbol.iterator] = function*() { yield 1; yield 2; yield 3; }[...[4, 5, 6]][1, 2, 3]iterator protocol की भयानक performance पर उस समय भी चर्चा हुई थी, लेकिन यह कहकर अनदेखा कर दिया गया कि escape analysis इसे हल कर देगा [0]. लगभग 10 साल बाद भी escape analysis ने इसे हल नहीं किया है, यह बहुत ज्यादा GC इस्तेमाल करता है और अब भी खास अच्छा नहीं है. यह performance के प्रति सचेत न रहने वाले लोगों द्वारा बनाई गई खराब specification है
अगर user ने
Symbol.iteratorpatch नहीं किया है, तो engine द्वारा array destructuring assignment और splicing को specialize करके iterator protocol overhead हटाना समझ में आ सकता है, लेकिन वह भी एक अलग सिरदर्द है[0] https://esdiscuss.org/topic/performance-of-iterator-next-as-...
और भी अजीब बात यह है कि कुछ JavaScript engines में दो-तीन elements वाले array को destructure करते समय
{0: foo, 1: bar}जैसी object destructuring तेज हो सकती हैSkew की convenience features खोने से होने वाले लगातार developer experience impact पर ज्यादा बात नहीं की गई है; बस codebase migration के समय एक बार किए गए conversion की बात है
उदाहरण के लिए TypeScript में आसानी से ऐसी स्थिति बन सकती है जहां files को सही order में import करना जरूरी हो जाता है, वरना चीजें टूट सकती हैं. साथ ही destructuring धीमी है, इसलिए performance ज़रा भी महत्वपूर्ण हो तो इसका इस्तेमाल नहीं करना चाहिए
TypeScript को कई साल इस्तेमाल करने पर दिखा कि ऐसे dozens pitfalls हैं; कुछ JavaScript से विरासत में आए हैं और कुछ नहीं. अगर engineers बहुत हैं, तो कम-से-कम एक बहुत बड़ा style guide चाहिए
सोचता हूं कि क्या कुछ engineers को Skew के हटने का अफसोस हुआ होगा
इससे developers PR review में देख सकते थे कि परिणाम कैसा दिखेगा और issues report कर सकते थे
performance और runtime correctness के लिहाज से TS में कुछ pitfalls हैं, यह सही है. array destructuring जैसी समस्याएं instrumentation और strict monitoring से पकड़ी गईं
Skew की कुछ features, जैसे operator overloading और integer types, का हटना निश्चित रूप से खला. लेकिन migration आखिरकार पूरी team का लिया हुआ फैसला था, और मुझे भी लगता है कि वह सही फैसला था
इसमें “async/await जैसे आधुनिक JavaScript features और ज्यादा flexible type system” लिखा है, तो क्या Skew में सिर्फ callbacks ही थे?
Promiseतो रहा होगा?Figma को अच्छी तरह न जानने के नाते, जिज्ञासा है कि वे WebAssembly क्यों इस्तेमाल करते हैं
Figma यह बेहद अच्छी तरह करता है. बहुत बड़े canvas पर, जहां कई complex UI screens हों, तब भी यह अविश्वसनीय रूप से responsive रहता है
आजकल के desktop applications में भी इतनी अच्छी तरह चलने वाले बहुत कम हैं. मुझे पूरा यकीन है कि इस तरह की optimization Figma की सफलता का अहम हिस्सा है
Figma ने security issue, यानी permissions को हल करने के लिए custom TypeScript DSL + compiler कैसे बनाया, इस बारे में दिलचस्प details एक और blog post में भी हैं
https://www.figma.com/blog/how-we-rolled-out-our-own-permiss...
थोड़ा तकलीफदेह लगता है. हर बड़ी company के पास अपने internal tools, language, Kubernetes जैसी चीजें होती हैं. वे इन्हें share क्यों नहीं करते
अगर Skew open source होता तो शायद वह बेहतर TypeScript बन सकता था
open source होने से मुफ्त contributions अपने-आप नहीं आ जाते. हर non-trivial PR के साथ लंबा review, discussion, और शायद rewrite तक करना पड़ता है
मुझे TypeScript पसंद है और TypeScript में full-stack system भी है, लेकिन यह परफेक्ट नहीं है। monorepo में TypeScript सेट करना किसी बुरे सपने जैसा है
pnpm monorepo के तहत internal packages के साथ इसे समझने लायक बनाने के लिए सभी paths को आपस में match कराने के लिए
tsconfig.jsonमें बहुत manual काम करना पड़ता हैऔर शानदार
tsxpackage आने से पहले production toolchain असल में maintain करना लगभग नामुमकिन थासाथ ही यह बेहद धीमा भी है। Zod की वजह से TypeScript language server performance काफी धीमी होने की समस्या थी, इसलिए अंत में project references अपनाने पड़े और project reference redirects बंद करने पड़े
कुल मिलाकर TypeScript को बेहतर बनाने की काफी गुंजाइश है। खासकर monorepo और performance के मामले में
monorepo वाली समस्या सुनने में ऐसी लगती है जैसे सभी packages में एक जैसी settings इस्तेमाल नहीं की गईं। अगर ऐसा है, तो पहले same settings और coding standards enforce करना ठीक करूंगा। यह भी TypeScript की समस्या नहीं है
क्या
tsxसे पहलेts-nodeइस्तेमाल कर रहे थे? अगर हां, तोtsxकहीं ज्यादा robust है। बस सही से काम करता हैएक medium-scale monorepo, कई apps और बहुत सारी services ज्यादातर TypeScript में चला रहा हूं, और boring npm workspaces based setup के साथ भी यह ठीक-ठाक काम करता है
इसका मतलब यह बिल्कुल नहीं कि TypeScript तेज है, लेकिन चूंकि यह JavaScript में लिखा गया है, इसलिए इसका तेज होना मुश्किल भी है
हालांकि बड़े applications में type inference के लिए TypeScript को code execute करने देना खुद बनाई हुई समस्या जैसा लगता है