۵۰ درصد تخفیف ویژه برای همه دوره‌ها از شنبه

مایکروسافت در حال انتقال کامپایلر تایپ اسکریپت به زبان Go است؛ تصمیمی که سرعت کامپایل را تا ۱۰ برابر افزایش می‌دهد. در این مقاله توضیح می‌دهیم چرا Go به‌جای زبان‌هایی مانند Rust یا #C انتخاب شده است، چرا به جای بازنویسی کامل، روش انتقال انتخاب شده و این تغییر چه پیامدهایی برای برنامه‌نویسان دارد؛ از جمله build سریع‌تر، بهبود عملکرد فرآیند CI/CD و پاسخ‌گویی بهتر ویرایشگرها.

مایکروسافت در یکی از مهم‌ترین اطلاعیه‌های دهه گذشته خبر داد که کامپایلر تایپ اسکریپت را به Go منتقل می‌کند؛ و جالب‌تر آنکه این جابه‌جایی کامپایلری تا ۱۰ برابر سریع‌تر به همراه خواهد داشت.

در ادامه، ابعاد مختلف این تصمیم و تأثیر آن بر تجربه توسعه‌دهندگان تایپ اسکریپت را بررسی می‌کنیم.

بررسی نوآوری‌های کلیدی در بازنویسی کامپایلر تایپ اسکریپت

زمانی که تایپ اسکریپت برای نخستین بار در سال ۲۰۱۲ طراحی شد، تیم توسعه آن تصمیم گرفت کامپایلر را با استفاده از خود زبان تایپ اسکریپت پیاده‌سازی کند. نتیجه این تصمیم آن بود که کدی که ما می‌نوشتیم، توسط کامپایلری که با همان زبان نوشته شده، پردازش می‌شد. دلیل این انتخاب، سهولت نگه‌داری و توسعه‌پذیری کامپایلر توسط جامعه توسعه‌دهندگان بود.

دلیل دیگر این بود که در سال ۲۰۱۲، استفاده اصلی از تایپ اسکریپت در توسعه رابط‌های کاربری بود، نه در برنامه‌هایی با محاسبات سنگین.

اما با رشد محبوبیت و پیچیدگی زبان، عملکرد کامپایلر به یکی از گلوگاه‌های اصلی برای بسیاری از توسعه‌دهندگان تبدیل شد. در پروژه‌های بزرگ، کامپایلر تایپ اسکریپت زمان زیادی برای ساخت و کامپایل کد صرف می‌کرد. در پروژه‌های چند میلیون خطی با سیستم تایپ‌دهی پیچیده، این مشکل کاملاً محسوس بود. برای نمونه، طبق داده‌های منتشرشده در وبلاگ رسمی، ریپازیتوری VS Code (با بیش از ۱.۵ میلیون خط کد) حدود ۷۷.۸ ثانیه برای کامپایل زمان نیاز داشت؛ که زمان قابل‌توجهی است.

این موضوع باعث شد تا نگه‌دارندگان پروژه به دنبال راه‌هایی برای بهبود عملکرد کامپایلر باشند. در نتیجه تیم تصمیم گرفت کامپایلر را به Go منتقل کند و این ابتکار را «پروژه Corsa» نامید. پس از انجام انتقال، همان repository تنها در ۷.۵ ثانیه کامپایل شد.

این مزایا فقط به پروژه‌های بزرگ مثل VS Code محدود نمی‌شود و در همه پروژه‌ها دیده می‌شود. به‌عنوان مثال، ریپازیتوری rxjs (با حدود ۲۱۰۰ خط کد) که پیش از این ۱.۱ ثانیه زمان نیاز داشت، پس از انتقال تنها ۰.۱ ثانیه زمان برد.

به عبارت دیگر، ما اکنون شاهد بهبود ۱۰ برابری در زمان کامپایل در طیف گسترده‌ای از پروژه‌ها هستیم.

چرا مایکروسافت به‌جای بازنویسی، کامپایلر تایپ اسکریپت را منتقل کرد؟

شاید این پرسش برایتان پیش آمده باشد که چطور چنین کدبیس عظیمی به‌سرعت با زبان Go پیاده‌سازی شده است؛ پاسخ ساده است: اصلاً از نو نوشته نشده است.

نکته هوشمندانه و قابل‌توجه در این رویکرد، عدم بازنویسی کامل کامپایلر از ابتداست. به‌جای آن، تمام کدهای موجود در repository به‌صورت برنامه‌نویسی‌شده، به معادل‌های خود در زبان Go تبدیل شده‌اند. به‌عبارت دیگر، اجزای مختلف کامپایلر تایپ اسکریپت مانند scanner، parser، binder و type checker به‌نوعی «منتقل» یا port شده‌اند.

یکی از نگه‌دارندگان اصلی تایپ‌اسکریپت در این لینک جزئیات بیشتری درباره علت انتخاب روش انتقال (porting) به‌جای بازنویسی کامل ارائه داده است. دلایل اصلی این تصمیم عبارت‌اند از:

مزیت مهم دیگر این رویکرد آن است که بخش زیادی از کدها می‌توانند به‌صورت خودکار و با استفاده از اسکریپت‌های ویژه به Go منتقل شوند. تنها قسمت‌های کلیدی و حساس، به‌صورت دستی بازنویسی شده‌اند. این موضوع با مقایسه فایل‌های متناظر در دو کدبیس کاملاً قابل مشاهده است.

به‌عنوان مثال، متدی با نام

reportCircularityError
reportCircularityError در فایل
checker.ts
checker.ts در کدبیس تایپ اسکریپت به‌صورت زیر می‌باشد:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
function reportCircularityError(symbol: Symbol) {
const declaration = symbol.valueDeclaration;
// Check if variable has type annotation that circularly references the variable itself
if (declaration) {
if (getEffectiveTypeAnnotationNode(declaration)) {
error(symbol.valueDeclaration, Diagnostics._0_is_referenced_directly_or_indirectly_in_its_own_type_annotation, symbolToString(symbol));
return errorType;
}
// Check if variable has initializer that circularly references the variable itself
if (noImplicitAny && (declaration.kind !== SyntaxKind.Parameter || (declaration as HasInitializer).initializer)) {
error(symbol.valueDeclaration, Diagnostics._0_implicitly_has_type_any_because_it_does_not_have_a_type_annotation_and_is_referenced_directly_or_indirectly_in_its_own_initializer, symbolToString(symbol));
}
}
else if (symbol.flags & SymbolFlags.Alias) {
const node = getDeclarationOfAliasSymbol(symbol);
if (node) {
error(node, Diagnostics.Circular_definition_of_import_alias_0, symbolToString(symbol));
}
}
return anyType;
}
function reportCircularityError(symbol: Symbol) { const declaration = symbol.valueDeclaration; // Check if variable has type annotation that circularly references the variable itself if (declaration) { if (getEffectiveTypeAnnotationNode(declaration)) { error(symbol.valueDeclaration, Diagnostics._0_is_referenced_directly_or_indirectly_in_its_own_type_annotation, symbolToString(symbol)); return errorType; } // Check if variable has initializer that circularly references the variable itself if (noImplicitAny && (declaration.kind !== SyntaxKind.Parameter || (declaration as HasInitializer).initializer)) { error(symbol.valueDeclaration, Diagnostics._0_implicitly_has_type_any_because_it_does_not_have_a_type_annotation_and_is_referenced_directly_or_indirectly_in_its_own_initializer, symbolToString(symbol)); } } else if (symbol.flags & SymbolFlags.Alias) { const node = getDeclarationOfAliasSymbol(symbol); if (node) { error(node, Diagnostics.Circular_definition_of_import_alias_0, symbolToString(symbol)); } } return anyType; }
function reportCircularityError(symbol: Symbol) {
    const declaration = symbol.valueDeclaration;
    // Check if variable has type annotation that circularly references the variable itself
    if (declaration) {
        if (getEffectiveTypeAnnotationNode(declaration)) {
            error(symbol.valueDeclaration, Diagnostics._0_is_referenced_directly_or_indirectly_in_its_own_type_annotation, symbolToString(symbol));
            return errorType;
        }
        // Check if variable has initializer that circularly references the variable itself
        if (noImplicitAny && (declaration.kind !== SyntaxKind.Parameter || (declaration as HasInitializer).initializer)) {
            error(symbol.valueDeclaration, Diagnostics._0_implicitly_has_type_any_because_it_does_not_have_a_type_annotation_and_is_referenced_directly_or_indirectly_in_its_own_initializer, symbolToString(symbol));
        }
    }
    else if (symbol.flags & SymbolFlags.Alias) {
        const node = getDeclarationOfAliasSymbol(symbol);
        if (node) {
            error(node, Diagnostics.Circular_definition_of_import_alias_0, symbolToString(symbol));
        }
    }

    return anyType;
}

معادل این متد در فایل

checker.go
checker.go در کدبیس GO به شکل زیر است:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
func (c *Checker) reportCircularityError(symbol *ast.Symbol) *Type {
declaration := symbol.ValueDeclaration
// Check if variable has type annotation that circularly references the variable itself
if declaration != nil {
if declaration.Type() != nil {
c.error(symbol.ValueDeclaration, diagnostics.X_0_is_referenced_directly_or_indirectly_in_its_own_type_annotation, c.symbolToString(symbol))
return c.errorType
}
// Check if variable has initializer that circularly references the variable itself
if c.noImplicitAny && (!ast.IsParameter(declaration) || declaration.Initializer() != nil) {
c.error(symbol.ValueDeclaration, diagnostics.X_0_implicitly_has_type_any_because_it_does_not_have_a_type_annotation_and_is_referenced_directly_or_indirectly_in_its_own_initializer, c.symbolToString(symbol))
}
} else if symbol.Flags&ast.SymbolFlagsAlias != 0 {
node := c.getDeclarationOfAliasSymbol(symbol)
if node != nil {
c.error(node, diagnostics.Circular_definition_of_import_alias_0, c.symbolToString(symbol))
}
}
return c.anyType
}
func (c *Checker) reportCircularityError(symbol *ast.Symbol) *Type { declaration := symbol.ValueDeclaration // Check if variable has type annotation that circularly references the variable itself if declaration != nil { if declaration.Type() != nil { c.error(symbol.ValueDeclaration, diagnostics.X_0_is_referenced_directly_or_indirectly_in_its_own_type_annotation, c.symbolToString(symbol)) return c.errorType } // Check if variable has initializer that circularly references the variable itself if c.noImplicitAny && (!ast.IsParameter(declaration) || declaration.Initializer() != nil) { c.error(symbol.ValueDeclaration, diagnostics.X_0_implicitly_has_type_any_because_it_does_not_have_a_type_annotation_and_is_referenced_directly_or_indirectly_in_its_own_initializer, c.symbolToString(symbol)) } } else if symbol.Flags&ast.SymbolFlagsAlias != 0 { node := c.getDeclarationOfAliasSymbol(symbol) if node != nil { c.error(node, diagnostics.Circular_definition_of_import_alias_0, c.symbolToString(symbol)) } } return c.anyType }
func (c *Checker) reportCircularityError(symbol *ast.Symbol) *Type {
    declaration := symbol.ValueDeclaration
    // Check if variable has type annotation that circularly references the variable itself
    if declaration != nil {
        if declaration.Type() != nil {
            c.error(symbol.ValueDeclaration, diagnostics.X_0_is_referenced_directly_or_indirectly_in_its_own_type_annotation, c.symbolToString(symbol))
            return c.errorType
        }
        // Check if variable has initializer that circularly references the variable itself
        if c.noImplicitAny && (!ast.IsParameter(declaration) || declaration.Initializer() != nil) {
            c.error(symbol.ValueDeclaration, diagnostics.X_0_implicitly_has_type_any_because_it_does_not_have_a_type_annotation_and_is_referenced_directly_or_indirectly_in_its_own_initializer, c.symbolToString(symbol))
        }
    } else if symbol.Flags&ast.SymbolFlagsAlias != 0 {
        node := c.getDeclarationOfAliasSymbol(symbol)
        if node != nil {
            c.error(node, diagnostics.Circular_definition_of_import_alias_0, c.symbolToString(symbol))
        }
    }


    return c.anyType
}

همان‌طور که می‌بینیم، هر خط از کد تایپ اسکریپت را می‌توانیم به سادگی با معادل آن در کد Go تطبیق دهیم، و این همان قدرت رویکرد port کردن است.

چرا زبان Go برای کامپایلر جدید تایپ اسکریپت انتخاب شد؟

پس از آنکه مشخص شد عملکرد کامپایلر یکی از گلوگاه‌های اصلی پروژه است، تیم تایپ اسکریپت بررسی گزینه‌های مختلفی را برای بهبود آن آغاز کرد. به گفته Anders Hejlsberg در یکی از ویدیویی‌های رسمی، چندین زبان برنامه‌نویسی مدنظر قرار گرفتند؛ از جمله Rust، سی‌شارپ (زبان داخلی محبوب مایکروسافت) و Go. هر یک از این زبان‌ها مزایا و چالش‌های خاص خود را داشتند.

چرا Rust بررسی شد؟

Rust یک زبان برنامه‌نویسی سیستمی است که به‌خاطر عملکرد بالا و ایمنی حافظه شناخته می‌شود. بسیاری از توسعه‌دهندگان برای پروژه‌هایی که به سرعت و کارایی بالا نیاز دارند، Rust را انتخاب می‌کنند؛ بنابراین، Rust یکی از گزینه‌های جدی برای بازنویسی کامپایلر بود.

چرا C# مطرح شد؟

C# زبان رسمی مایکروسافت است و در بسیاری از محصولات این شرکت مورد استفاده قرار می‌گیرد. انتخاب این زبان می‌توانست به تیم کمک کند تا از دانش موجود، ابزارهای داخلی و زیرساخت‌های مایکروسافت بهره بیشتری ببرد.

چرا در نهایت Go انتخاب شد؟

در نهایت، تیم تصمیم گرفت که کامپایلر تایپ اسکریپت را با زبان Go منتقل کند. Go به‌خاطر کامپایل سریع و مصرف کم حافظه شهرت دارد. دلایل فنی دیگری نیز در این تصمیم مؤثر بودند، از جمله:

با این حال، مهم‌ترین عامل برتری Go نسبت به سایر گزینه‌ها، شباهت معنایی زیاد آن به تایپ اسکریپت و امکان انتقال ساده‌تر کدها بود؛ نکته‌ای که در بخش قبل نیز به آن اشاره شد. این شباهت نقش مهمی در تصمیم‌گیری نهایی ایفا کرد.

دستاوردهای عملکردی و ملاحظات کامپایلر مبتنی بر Go

Hejlsberg اشاره کرده که بیشتر بخش‌های مربوط به port کردن کامپایلر تکمیل شده‌اند و type checker نیز تاکنون حدود ۸۰٪ پیشرفت داشته است.

در حال حاضر تمرکز اصلی توسعه روی بخش language service قرار دارد.

هرچند عمده بهبودهای عملکردی به استفاده از یک زبان native مانند Go مربوط می‌شود، سایر بهینه‌سازی‌ها نیز در این بهبود نقش داشته‌اند. یکی از مهم‌ترین آن‌ها استفاده از هم‌زمانی (concurrency) است؛ برای مثال، اجرای هم‌زمان چهار نمونه از type checker به‌جای تنها یکی.

بهبود دیگر، بازطراحی language service برای انطباق بهتر با پروتکل Language Server است. این موارد در کنار هم، تجربه توسعه را برای برنامه‌نویسان تایپ اسکریپت به‌شکلی چشمگیر بهبود خواهند داد.

پروتکل سرور زبان (LSP)

Language Server Protocol (LSP) امروزه به‌طور گسترده در سرویس‌های زبان مدرن استفاده می‌شود. اما زمانی که تایپ اسکریپت برای اولین‌بار معرفی شد، LSP هنوز وجود نداشت. اکنون با انتقال تایپ اسکریپت به Go، تیم توسعه این فرصت را یافته است تا معماری سرویس زبان را بازطراحی کند و آن را با استانداردهای LSP هماهنگ سازد.

انتظار برنامه‌نویسان از کامپایلر جدید تایپ اسکریپت

یکی از مزایای اصلی این تغییر، افزایش چشمگیر عملکرد بدون نیاز به دخالت توسعه‌دهنده است. به‌محض اینکه یک برنامه‌نویس، تایپ اسکریپت را به نسخه ۷ به‌روزرسانی کند، کامپایلر جدید مبتنی بر Go به‌طور خودکار هنگام اجرای

tsc
tsc مورد استفاده قرار خواهد گرفت.

همچنین انتظار می‌رود یک language service سریع‌تر در اختیار توسعه‌دهندگان قرار گیرد؛ سرویسی که در ویرایشگرهایی مانند VS Code برای قابلیت‌هایی مثل IntelliSense، ناوبری کد و موارد مشابه استفاده می‌شود. این بهبود، باعث افزایش سرعت راه‌اندازی و پاسخ‌دهی ویرایشگر کد خواهد شد.

افزایش سرعت Build در پایپ‌لاین‌های CI/CD

یکی از مزایای فوری کامپایلر جدید، کاهش زمان build پروژه‌ها است؛ به‌ویژه در پایپ‌لاین‌های CI/CD که این موضوع می‌تواند زمان اجرای تست‌ها و فرایند استقرار را به‌شکلی محسوس کاهش دهد و در نتیجه، چرخه بازخورد سریع‌تری ایجاد کند.

بهبود سرعت بارگذاری و بررسی کد در ویرایشگر

زمانی که یک repository بزرگ تایپ اسکریپت در ویرایشگری مانند VS Code باز می‌شود، فرآیندهایی مانند بارگذاری فایل‌ها، تنظیم لینک‌ها بین آن‌ها و فعال‌سازی IntelliSense می‌توانند زمان‌بر باشند. با استفاده از کامپایلر جدید، این فرآیندها به‌شکل قابل توجهی سریع‌تر انجام می‌شوند. حتی فرآیند lint کردن کدها نیز بهبود می‌یابد، به‌طوری‌که خطوط هشدار قرمز رنگ (که معمولاً برای همه توسعه‌دهندگان آزاردهنده‌اند!) سریع‌تر نمایش داده می‌شوند.

افزایش سرعت بارگذاری مجدد (Hot Reload)

یکی دیگر از حوزه‌هایی که توسعه‌دهندگان بهبود آن را احساس خواهند کرد، سرعت بیشتر در بارگذاری مجدد است. وقتی کدی تغییر می‌کند و ذخیره می‌شود، مرورگر می‌تواند تغییرات را سریع‌تر نشان دهد. دلیل این موضوع آن است که کامپایلر جدید می‌تواند فایل‌هایی را که به‌صورت جزئی تغییر کرده‌اند، با سرعت بیشتری پردازش کند.

نقشه راه و جدول زمانی نسخه ۷ تایپ‌اسکریپت

انتشار نسخه ۵.۹ تایپ اسکریپت به‌زودی در راه است، و تا پایان سری نسخه‌های ۶.x، کامپایلر همچنان با خود تایپ اسکریپت نوشته خواهد شد. در این دوره، برخی ویژگی‌ها از رده خارج می‌شوند و تغییرات مخربی برای آماده‌سازی مسیر انتقال به کامپایلر جدید اعمال خواهند شد. نسخه کاملاً native مبتنی بر Go، هم‌زمان با انتشار نسخه ۷ تایپ اسکریپت عرضه خواهد شد.

جمع‌بندی

Anders Hejlsberg و تیمش کاری شبیه به دویدن ۱۶۰۰ متر در چهار دقیقه را برای دنیای تایپ اسکریپت انجام داده‌اند!

این موفقیت، نقطه عطفی در تاریخچه تایپ اسکریپت به‌شمار می‌رود. در حالی‌که مزایای عملکردی این کامپایلر به‌زودی به توسعه‌دهندگان و کل اکوسیستم منتقل خواهد شد، این حرکت می‌تواند الهام‌بخش سایر پروژه‌ها و کتابخانه‌های وابسته به تایپ اسکریپت باشد تا مرزهای ممکن را جابه‌جا کنند.