TypeScript Going x10 Faster
TypeScript לא מאיצה רק build. אריאל שולמן מפרקת את הקומפיילר, ומראה איך Go, עצי AST ו-parallelism מקצרים את ההמתנה בקודבייסים גדולים - גם בתוך ה-IDE.
צפו בהרצאההסיפור שמאחורי ההרצאה
- TypeScript היא קודם כל כלי פיתוח: היא מחזיקה language server חי ב-IDE, ולכן זמן התגובה בזמן כתיבה חשוב לא פחות מזמן ה-build.
- הקומפיילר עובד בארבעה שלבים: parsing, binding, checking ו-emitting. type checking הוא החלק הכבד, כי הוא תלוי בקונטקסט של כל הקודבייס.
- המעבר ל-Go אינו רק החלפת שפה. parsing, binding ו-emitting יכולים לרוץ במקביל לכל קובץ, ו-type checking מחלק את העבודה על עצי AST משותפים לקריאה בלבד.
- גם מי שכותב JavaScript או משתמש ב-esbuild ו-SWC עדיין פוגש TypeScript דרך ה-language server והמידע שה-IDE מסיק על הקוד.
TypeScript לא נתקעת רק כשמריצים build. היא נתקעת ברגע הפחות נסבל: כשאתם מחכים ל-hover, ל-autocompletion או לעוד טיפת קונטקסט בזמן שהקוד עדיין חצי כתוב. בקודבייס גדול, ההמתנה הזאת הופכת את כלי הפיתוח עצמו לצוואר הבקבוק.
אריאל שולמן פותחת את הקומפיילר של TypeScript ומפרקת את ההבטחה ל-פי 10 מהירות. לא מדובר בקסם ולא רק ב”שפה מהירה יותר”. זה porting מדויק ל-Go, ארכיטקטורה שמפרידה בין קבצים, ועבודה מקבילית במקום שבו אפשר להריץ אותה בלי לשבור את הנכונות של type checking.
TypeScript היא לא רק transpiler
אפשר לתאר את TypeScript כ-transpiler שמקבל TypeScript ומוציא JavaScript. זה נכון, אבל זו רק שכבה אחת. בהרצאה אריאל מציבה אותה קודם כל ככלי פיתוח: השפה קיימת כדי לתת למפתח חוויה טובה יותר ב-.ts וב-.tsx.
לכלי הזה יש שני מצבים עם דרישות הפוכות. ב-DevTime רץ language server חי בתוך ה-IDE. הוא צריך לתת מידע על טיפוסים, ניווט לקבצים ו-autocompletion בזמן שהקוד משתנה, ולעתים קרובות גם שבור. ב-BuildTime אפשר להניח שהקוד כבר מוכן, ואז לייצר JavaScript, declaration files ו-source maps.
הפער אינו קוסמטי. build של כמה שניות נסבל. המתנה של שנייה וחצי ל-autocompletion כבר שוברת את חוויית העבודה. לכן מהירות הקומפיילר היא לא מדד נפרד מ-DX. היא ה-DX.
ארבעה שלבים לפני שהקוד הופך ל-JavaScript
אריאל מפרקת את העבודה של TypeScript לארבעה שלבים. parsing הופך source code ל-AST - עץ שמתאר את תוכנית הקוד. binding מחבר בין שימושים להגדרות שלהם. checking בודק חוזי טיפוסים וגם מסיק טיפוסים שלא הוגדרו במפורש. emitting מחזיר קוד, ומבצע גם transformations כדי להתאים ל-target שהוגדר ב-tsconfig.
החלק הכבד אינו רק “להסיר טיפוסים”. כשעושים hover על ערך, TypeScript יכולה לטייל מהשימוש בו אל פונקציה, אל ערך מוחזר, אל ארגומנט ואל הטיפוס שלו. בשתי שורות זה כבר מסלול. על מאות קבצים הוא נהיה בעיית performance אמיתית.
המורכבות עולה כשמחלקים קוד לקבצים. לכל קובץ נוצר AST משלו, ובשלב ה-binding נוצרות Symbol Lookup Tables שמאפשרות למצוא במהירות import, export והגדרות קשורות. זו גם הסיבה שקודבייס גדול לא יכול להתנהג כמו עץ יחיד ופשוט.
גם JavaScript כבר משתמשת ב-TypeScript
“אני כותב JavaScript” או “אני משתמש ב-esbuild” לא מוציאים את TypeScript מהתמונה. אריאל מדגימה שגם בקובץ JavaScript, hover ב-IDE יכול להציג את הטיפוס של ערך. מאחורי הפעולה הזאת ה-language server של TypeScript מטייל באותם קשרים בין קבצים ומסיק את המידע.
ב-BuildTime אפשר לבחור בכלים כמו esbuild או SWC, שמבצעים emitting ולא type checking. זה עובד משום שהצוות סומך על המעגל שכבר קרה ב-DevTime. אבל ה-IDE עדיין צריך להבין את הקוד. לכן קיצור זמן של TypeScript נוגע גם למפתחים שלא מפעילים tsc כשלב ה-build שלהם.
המחיר בולט במיוחד בפרויקטים גדולים. ב-הדיון על מונוריפו, מולטי-ריפו ומה שביניהם גיל תייר עוסק בבחירות סביב מבנה קודבייסים. כאן אריאל מראה את הצד השני: ככל שיש יותר קבצים וקשרים, כלי הפיתוח צריך לשמר תמונה שלמה בלי לגרום למפתח לחכות לה.
למה porting ל-Go שינה את התמונה
הצוות לא יצא ל-migration שמזמין refactor וכתיבה מחדש. הוא עשה porting: שימור הקוד הקיים והעברתו לשפה אחרת ככל האפשר בצורה אוטומטית. לפי אריאל, זו הסיבה לבחירה ב-Go - דרך מעשית להעביר קודבייס שנבנה במשך יותר מעשור בתוך כשנה.
כ-30% מהשיפור מגיעים מכך ש-Go היא שפה native. אבל זה לא הסיפור המרכזי. parsing ו-binding עובדים ברמת הקובץ: כל קובץ יכול להיכנס ל-goroutine משלו, לעבור parsing ו-binding במקביל, ולהחזיר AST ומפה. גם emitting עובד ברמת הקובץ וניתן לחלוקה דומה.
הבעיה האמיתית היא type checking. אי אפשר פשוט לתת לכל goroutine קובץ, כי תקינות של קובץ תלויה בקונטקסט של קבצים אחרים. ובכל זאת, בשלב הזה ה-ASTים רק נקראים - הם אינם משתנים. לכן אפשר לשתף אותם כזיכרון משותף לקריאה בלבד, לחלק את הקודבייס לחלקים, ולבדוק אותם במקביל מול אותו מבנה.
זו גם הסיבה ש-JavaScript לא הייתה מספיקה למהלך הזה. העברת עץ של מיליוני צמתים בין תהליכים דרך serialization ו-deserialization הייתה עולה יותר מדי. Go מאפשרת את השיתוף ואת המקביליות בלי להפוך את עצם התקשורת לעבודה הכבדה.
הנקודה החשובה היא שהחלוקה אינה הבטחה שכל פעולה הופכת לעצמאית. היא נשענת על הגבולות שכבר קיימים בקומפיילר: קובץ יכול להיבנות בפני עצמו, אבל בדיקת הטיפוסים שלו עדיין קוראת הקשרים רחבים. לכן האופטימיזציה שומרת על הסמנטיקה, במקום להחליף אותה בקיצור דרך שמחזיר תשובה מהירה אך לא נכונה.
טייפסקריפט היא בראש ובראשונה כלי פיתוח.
מה באמת משתנה למפתחים
השיפור אינו הבטחה להחליף כלי אחד באחר. הוא ניסיון להסיר tradeoff ישן: לבחור בין תמונת טיפוסים מלאה לקודבייס גדול לבין סביבת עבודה שמגיבה בזמן. TypeScript צריכה לזרוק עבודה ולהתחיל מחדש כשמגיעה הקלדה חדשה, ולכן caching לפי קובץ ועצים נפרדים אינם פרט מימוש שולי - הם הדרך לשמור על ה-IDE שימושי.
הבילד טיים משרת מכונות והדב טיים משרת בני אדם.
הדרך הנכונה לקרוא את ה-פי 10 היא לא “עכשיו כל build יהיה פי 10 מהיר”. זו תוצאה של הרצה native ושל חלוקה נכונה של עבודה: במקביל היכן שהקבצים עצמאיים, ובזיכרון משותף לקריאה בלבד היכן שהסמנטיקה דורשת הקשר רחב. זו ארכיטקטורת קומפיילר שנבנתה סביב המפתח שיושב מול העורך.
השורה התחתונה
- TypeScript משרתת את ה-IDE ואת ה-build, אבל הדרישה הקשוחה ביותר שלה היא תגובה מהירה בזמן כתיבה.
- parsing, binding, checking ו-emitting הם שלבים שונים עם אפשרויות שונות ל-parallelism.
- עצי AST ומפות סמלים לפי קובץ מאפשרים caching ועבודה יעילה על קוד שמשתנה.
- Go מביאה גם הרצה native וגם מודל שמאפשר להריץ חלקים מהקומפיילר במקביל.
- type checking נשאר מורכב, ולכן ההקבלה שלו נשענת על עצים משותפים לקריאה בלבד ולא על בידוד מוחלט בין קבצים.
הצ’קינג הוא באמת החלק הכי הכי הכי בעייתי.
ההרצאה מתאימה למי שרוצה להבין למה ה-IDE מרגיש איטי דווקא כשהקוד גדל, מה באמת קורה מאחורי hover, ואיך שינוי שפה בקומפיילר יכול לשפר את העבודה היומיומית בלי לשנות את השפה שאתם כותבים.
"טייפסקריפט היא בראש ובראשונה כלי פיתוח."
- אריאל שולמן
"הבילד טיים משרת מכונות והדב טיים משרת בני אדם."
- אריאל שולמן
"הצ'קינג הוא באמת החלק הכי הכי הכי בעייתי."
- אריאל שולמן
- 00:01:40 למה TypeScript עברה ל-Goאריאל מסבירה את ההבדל בין porting ל-migration, ולמה הצוות רצה לשמר קודבייס שנבנה יותר מעשור במקום לכתוב אותו מחדש.
- 00:05:37 TypeScript היא כלי פיתוח לפני שהיא שפהההרצאה מפרידה בין העבודה ב-IDE לבין ה-build, וממקמת את חוויית הפיתוח כמטרת המוצר.
- 00:14:17 ארבעת שלבי הקומפיילרparsing יוצר AST, binding מחבר סמלים, checking בודק ומסיק טיפוסים, ו-emitting מייצר JavaScript ומבצע transformations.
- 00:21:19 למה כל קובץ מקבל עץ משלופיצול הקוד לקבצים דורש Symbol Lookup Tables, אבל גם מאפשר caching של עצים שלא השתנו בזמן העבודה ב-IDE.
- 00:28:30 למה גם JavaScript נהנה מהשיפורה-language server של TypeScript מנתח גם קבצי JavaScript כדי לתת hover ומידע על טיפוסים, ולכן המהירות שלו משפיעה מעבר לקבצי .ts.
- 00:32:44 Go, parallelism ו-type checkingכ-30% מהשיפור מגיעים מהרצה native, אך עיקרו מגיע מהרצת שלבים לפי קובץ במקביל ומשיתוף עצי AST לקריאה בלבד בין goroutines.
מה כוסה בהרצאה
שאלות מההרצאה
למה TypeScript עברה מ-TypeScript ל-Go?
לפי אריאל, זו הייתה עבודת porting ולא כתיבה מחדש: המטרה הייתה לשמר ככל האפשר קודבייס שנבנה יותר מעשור. Go נבחרה כי אפשרה להעביר את הקוד בצורה אוטומטית יחסית, ובמקביל להרוויח הרצה native ועבודה מקבילית.
מה ההבדל בין TypeScript ב-DevTime וב-BuildTime?
ב-DevTime ה-language server חי בתוך ה-IDE, מתמודד גם עם קוד חלקי או שבור, וצריך להגיב כמעט מיד ל-hover ול-autocompletion. ב-BuildTime אפשר להריץ את כל שלבי הקומפיילר, או רק emitting אם type checking כבר בוצע בזמן הפיתוח.
למה type checking קשה יותר להקבלה?
parsing ו-binding מייצרים עץ ומפה לכל קובץ ולכן אפשר לעבד קבצים במקביל. לעומת זאת, הנכונות של type checking תלויה בקונטקסט של שאר הקבצים. הפתרון שמוצג בהרצאה מחלק את הקודבייס לחלקים, בעוד עצי ה-AST משותפים לקריאה בלבד.
האם שיפור מהירות של TypeScript רלוונטי גם למי שכותב JavaScript?
כן. אריאל מראה שה-language server של TypeScript פועל גם מאחורי hover ומידע על טיפוסים בקבצי JavaScript. בנוסף, כלים כמו esbuild ו-SWC יכולים להפיק JavaScript בלי לבדוק טיפוסים, אבל חוויית הפיתוח עדיין נשענת על העבודה שכבר נעשתה ב-DevTime.