基于 Rust 的 linter 工具速度很快,但有嚴(yán)重缺陷...
2023 年 Web 工具的一大趨勢(shì)是使用 Rust 重寫現(xiàn)有工具。Rust 是一種出色的編程語(yǔ)言,能生成運(yùn)行速度驚人的二進(jìn)制文件,且與其它 Web 工具的互操作性極佳,這得益于 WebAssembly 的幫助。swc 和 Turbopack 等工具的速度提升為快速開(kāi)發(fā)體驗(yàn)帶來(lái)了巨大變革。
Biome、deno lint、Oxc 和 RSLint 等項(xiàng)目都有一個(gè)用 Rust 編寫的 JavaScript/TypeScript 代碼檢查器。對(duì)于那些對(duì)開(kāi)發(fā)工具速度緩慢感到不滿的開(kāi)發(fā)人員來(lái)說(shuō),以Rust(本機(jī)代碼)速度運(yùn)行代碼檢查器,而非JavaScript(JIT腳本)速度,無(wú)疑是很有吸引力的。Prettier 甚至為 Biome 提供了 20,000 美元的獎(jiǎng)金,以表彰其實(shí)現(xiàn)了與 Prettier 格式部分的 >95% 兼容性!
然而,基于 Rust 的 linters 是無(wú)法完全取代 ESLint 的。雖然性能優(yōu)勢(shì)明顯,但也存在一個(gè)明顯的缺陷:類型檢查的 linting 功能缺失。
回顧:類型檢查的 Linting
傳統(tǒng)上,像 ESLint 這樣的 lint 工具一次只能檢查一個(gè)源代碼文件。這使得它們運(yùn)行速度較快,理論上可以進(jìn)行緩存和并行處理。
typescript-eslint 引入了使用類型信息的 linting 概念。通過(guò)調(diào)用 TypeScript 的類型檢查 API,lint 規(guī)則可以根據(jù)項(xiàng)目中的其他文件提供的類型信息,對(duì)代碼做出更加明智的決策。
類型檢查的 lint 規(guī)則可能比傳統(tǒng)的 lint 規(guī)則功能更強(qiáng)大。例如:
- @typescript-eslint/await-thenable :禁止在非 Promise 值上使用不必要的 await 調(diào)用。
- @typescript-eslint/no-floating-promises :可以提醒是否創(chuàng)建了一個(gè) Promise 但忘記安全地處理它。
- @typescript-eslint/no-for-in-array :用于標(biāo)記對(duì)數(shù)組的不安全的 for...in 迭代(不是 for...of)。
這些規(guī)則只有在能夠使用類型信息來(lái)確定何時(shí)報(bào)告問(wèn)題時(shí)才有實(shí)際用處。沒(méi)有類型信息,它們將無(wú)法理解從另一個(gè)模塊導(dǎo)入的值的類型。
類型檢查的 Linting 性能
類型檢查的 linting 相比傳統(tǒng) linting 存在的主要劣勢(shì)在于性能。這是因?yàn)轭愋突?lint 規(guī)則需要調(diào)用 TypeScript 等 API 來(lái)獲取類型信息,通常需要讀取所有文件以確定哪些文件會(huì)影響其他文件的類型。因此,類型檢查的 linting 性能通常會(huì)低于對(duì)整個(gè)項(xiàng)目運(yùn)行 TypeScript 的性能。
TypeScript 本身也在不斷優(yōu)化性能。例如,項(xiàng)目引用可以顯著幫助處理更大的項(xiàng)目。TypeScript 即將推出的獨(dú)立聲明模式看起來(lái)也可以顯著提高處理更大項(xiàng)目的性能。
但即使所有這些加速都完美地工作,類型檢查的 linting 設(shè)計(jì)上仍然比傳統(tǒng)的 linting 慢幾個(gè)數(shù)量級(jí)。因?yàn)閺捻?xiàng)目中推斷類型的過(guò)程本質(zhì)上比傳統(tǒng)的 lint 規(guī)則一次只查看一個(gè)文件要慢得多。
大多數(shù)情況下,當(dāng)看到類型檢查的速度慢的項(xiàng)目時(shí),根本原因要么是 typescript-eslint 配置錯(cuò)誤,要么是 TypeScript 類型慢。
基于 Rust 的代碼檢查工具和類型檢查
目前還沒(méi)有基于 Rust 的代碼檢查器與 TypeScript 的類型檢查 API 集成,這意味著基于 Rust 的代碼檢查器不能完全替代 ESLint + typescript-lint。
如果你不需要任何類型檢查的 lint 規(guī)則,那么可以切換到基于 Rust 的 linter。但強(qiáng)烈建議你至少查看 typescript-lint 中推薦的類型檢查規(guī)則,以了解缺少什么。
甚至可以同時(shí)運(yùn)行這兩種工具:首先使用原生速度的 linter 快速反饋,然后僅使用 typescript-eslint 查看包含類型信息的規(guī)則。這個(gè)想法得到了多個(gè)原生速度 linter 維護(hù)者的支持:
- Biome 的 Emanuele 認(rèn)為雙重 linting 是一種合理的策略。
- Oxc 的公告將 oxlint 描述為在 ESLint 過(guò)慢時(shí)的增強(qiáng)工具,而不是完全替代品。
這種互補(bǔ)而非取代的愿望部分源于這兩種 lint 工具在運(yùn)作方式上的重大結(jié)構(gòu)性差異。原生速度的 lint 工具尚未在其 lint 規(guī)則中實(shí)現(xiàn)類型檢查。下面來(lái)深入探討這一奇怪的功能差距。
集成類型檢查的 Linting 和基于 Rust 的 Linting
目前,TypeScript 的核心功能是為 TypeScript 編譯器和語(yǔ)言服務(wù)提供支持的代碼,它是唯一能夠?yàn)?TypeScript 代碼提供可靠類型檢查的組件。由于TypeScript是用TypeScript編寫的,因此其類型檢查以JavaScript的速度運(yùn)行。
為了實(shí)現(xiàn)與 TypeScript 的類型檢查的集成,基于Rust的代碼檢查器面臨幾個(gè)選擇:
- 承受性能損失,調(diào)用TypeScript的JavaScript速度類型檢查API。
- 使用原生速度語(yǔ)言重新實(shí)現(xiàn)TypeScript的API。
- 將 TypeScript 的 API 提升到原生速度。
此外,基于 Rust 的 linter 不允許在 JavaScript 中編寫自定義 lint 規(guī)則。雖然這對(duì)大多數(shù) JavaScript 生態(tài)系統(tǒng)來(lái)說(shuō)是一個(gè)貢獻(xiàn)障礙,但這與本文的重點(diǎn)是兩個(gè)獨(dú)立的問(wèn)題。
因此,將基于 Rust 的代碼檢查器與 TypeScript 的類型檢查集成在一起有不同的選項(xiàng)。
降低 JavaScript 速度
選擇這種性能影響方案可能會(huì)使基于 Rust 的 linter 速度降低到幾乎與 ESLint 無(wú)明顯性能優(yōu)勢(shì)的程度。
以原生速度重新實(shí)現(xiàn) TypeScript
對(duì)于 TypeScript 用戶來(lái)說(shuō),以原生速度重新實(shí)現(xiàn) TypeScript 是一個(gè)極具吸引力的前景,而不僅僅是對(duì)于 linter。目前已有三個(gè)重要的嘗試:
- Ezno:一種類似于 TypeScript 的新語(yǔ)言,增加了依賴類型等特性。
- stc:一個(gè)可以替代 TypeScript 類型檢查的 Rust 編寫項(xiàng)目。
- TypeRunner:一個(gè)較早的嘗試,使用 C++ 編寫,但已不再積極開(kāi)發(fā)。
需要注意的是,以新語(yǔ)言重新實(shí)現(xiàn) TypeScript 是一項(xiàng)艱巨的任務(wù)。TypeScript 的類型推理需要處理泛型類型、協(xié)變、逆變等復(fù)雜邊緣情況,這是一項(xiàng)極具挑戰(zhàn)性的任務(wù)。這些項(xiàng)目目前都處于非常早期的階段,可能需要很長(zhǎng)時(shí)間才能準(zhǔn)備投產(chǎn)。
那是否可以通過(guò)縮小項(xiàng)目的范圍,只實(shí)現(xiàn)TypeScript的類型推理部分,從而降低這一選項(xiàng)的復(fù)雜性呢?對(duì)于 linters 來(lái)說(shuō),一個(gè)簡(jiǎn)化版的TypeScript,跳過(guò)源代碼轉(zhuǎn)換、類型檢查可分配性錯(cuò)誤等部分,只專注于編程類型檢查API,或許更為實(shí)用。例如,Oxc 項(xiàng)目已經(jīng)成功地實(shí)現(xiàn)了一個(gè) TypeScript 類型推理的簡(jiǎn)化版,僅用幾千行Rust代碼就完成了這一任務(wù)。
然而,我們必須正視TypeScript背后有一個(gè)強(qiáng)大的開(kāi)發(fā)團(tuán)隊(duì)和社區(qū)支持的現(xiàn)實(shí)。TypeScript團(tuán)隊(duì)由專業(yè)的編程語(yǔ)言專家組成,并且持續(xù)從社區(qū)中獲得貢獻(xiàn)。對(duì)于任何嘗試重新實(shí)現(xiàn)TypeScript的項(xiàng)目來(lái)說(shuō),跟上TypeScript的更新步伐是一項(xiàng)幾乎不可能完成的任務(wù)。盡管Ezno和stc等項(xiàng)目展現(xiàn)了令人印象深刻的成果,但它們作為獨(dú)立項(xiàng)目的長(zhǎng)期可行性仍然充滿了不確定性。
將 TypeScript 的 API 提升到原生速度
為了提高TypeScript的性能,一個(gè)更具可行性的長(zhǎng)期方案是優(yōu)化其類型檢查器的運(yùn)行速度。目前有幾種可能的解決方案:
- 將TypeScript的類型檢查器轉(zhuǎn)換為更高效的編程語(yǔ)言,如Go或Rust。這可以通過(guò)編寫一個(gè)轉(zhuǎn)換工具來(lái)實(shí)現(xiàn),將TypeScript源代碼轉(zhuǎn)換為這些更快的語(yǔ)言。
- 對(duì)TypeScript進(jìn)行預(yù)編譯和優(yōu)化,類似于將其轉(zhuǎn)換為二進(jìn)制格式。這種方法可以在編譯時(shí)對(duì)代碼進(jìn)行優(yōu)化,以提高運(yùn)行時(shí)的性能。
- 利用Node.js的用戶快照技術(shù)來(lái)優(yōu)化啟動(dòng)時(shí)間。通過(guò)在啟動(dòng)時(shí)預(yù)先優(yōu)化代碼,可以加快冷啟動(dòng)編譯器的速度。
- AssemblyScript和Static TypeScript是另外兩個(gè)有趣的探索方向,它們通過(guò)使用TypeScript的子集或修改版本來(lái)關(guān)注低級(jí)性能。
這些方案都面臨一定的挑戰(zhàn),需要投入時(shí)間和資源進(jìn)行開(kāi)發(fā)。然而,通過(guò)持續(xù)優(yōu)化和改進(jìn),可以逐步提高TypeScript的性能,使其更加適應(yīng)快速發(fā)展的開(kāi)發(fā)需求。
雖然可以通過(guò)各種方法來(lái)加速TypeScript的運(yùn)行,但其實(shí)TypeScript本身的架構(gòu)是阻礙性能提升的主要因素。它的代碼基于一種假設(shè),即運(yùn)行時(shí)環(huán)境將提供內(nèi)置的垃圾回收、可變對(duì)象等功能,而這些功能往往會(huì)帶來(lái)性能上的損耗。
為了真正提高TypeScript的性能,我們可能需要重新設(shè)計(jì)其架構(gòu),使其更加適應(yīng)高性能場(chǎng)景:
- 隔離聲明模式:這可能是最直接的方法,通過(guò)將類型聲明與實(shí)際代碼隔離,可以減少編譯時(shí)的計(jì)算量,從而提高運(yùn)行速度。
- 優(yōu)化全局類型擴(kuò)展:為了更好地支持并行化,我們需要限制全局類型擴(kuò)展的使用,以減少潛在的性能瓶頸。
- 改進(jìn)檢查器運(yùn)行方式:通過(guò)改變TypeScript檢查器的運(yùn)行方式,可以避免一些不必要的性能損耗,進(jìn)一步提高運(yùn)行速度。
然而,任何對(duì)TypeScript結(jié)構(gòu)的重大更改都可能導(dǎo)致其API的重大變化,并可能引入新的問(wèn)題。目前看來(lái),除了可能在2024年推出的隔離聲明模式外,其他的大規(guī)模改動(dòng)短期內(nèi)不太可能實(shí)現(xiàn)。
TypeScript 集成 Linting
另一個(gè)策略是將 linting 集成到現(xiàn)有的 TypeScript 語(yǔ)言服務(wù)器基礎(chǔ)架構(gòu)中。TypeScript 語(yǔ)言服務(wù)插件允許添加工具作為 TypeScript 編輯體驗(yàn)的一部分運(yùn)行。
可以看到過(guò)兩次嘗試:
- Quramy/typescript-??-language-service:ESLint 的通用 TypeScript 語(yǔ)言服務(wù)插件
- johnsoncodehk/typescript-linter:基于 TypeScript 語(yǔ)言服務(wù)器構(gòu)建的代碼檢查器的重新實(shí)現(xiàn)
兩者似乎都有希望。為了與現(xiàn)有規(guī)則兼容,在短期內(nèi)將 ESLint 作為 TypeScript 語(yǔ)言服務(wù)插件運(yùn)行是更可行的。無(wú)論哪種方式,在不落后于其他語(yǔ)言的情況下,如何使 TypeScript 體驗(yàn)變得更好,尤其是考慮到 ESLint 打算擁抱其他 Web 語(yǔ)言,這將是一個(gè)關(guān)鍵挑戰(zhàn)。
小結(jié)
基于 Rust 的 JavaScript/TypeScript 代碼檢查器,如 Biome、deno lint、Oxc 和 RSLint,都是非常快速的項(xiàng)目。但與 ESLint + typescript-ndrings 的類型檢查代碼規(guī)則相比,這種速度存在嚴(yán)重的功能差距。在決定使用哪個(gè)工具時(shí),你應(yīng)該了解這些權(quán)衡。Biome 和 oxlint 都表示在一定程度上建議先運(yùn)行一個(gè)更快的原生速度代碼檢查器,而不是運(yùn)行基于類型的 typescript-lint。
基于 Rust 的 linter 最終可能會(huì)以原生速度代碼獲得類型檢查 linting 的好處。但要實(shí)現(xiàn)這一點(diǎn)還有很長(zhǎng)的路要走。