亚洲乱码中文字幕综合,中国熟女仑乱hd,亚洲精品乱拍国产一区二区三区,一本大道卡一卡二卡三乱码全集资源,又粗又黄又硬又爽的免费视频

JS生態(tài)系統(tǒng)加速eslint解析器使用實(shí)例探索

 更新時(shí)間:2024年01月21日 11:46:52   作者:大家的林語(yǔ)冰 人貓神話  
這篇文章主要為大家介紹了JS生態(tài)系統(tǒng)加速之eslint解析器使用實(shí)例探索,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪

引言

長(zhǎng)話短說(shuō):Linting 是在代碼中查找模式的行為,這可能導(dǎo)致錯(cuò)誤或確保一致的閱讀體驗(yàn)。它是一大坨 JS/TS 項(xiàng)目的核心部分。我們發(fā)現(xiàn)其選擇器引擎和 AST 轉(zhuǎn)換過(guò)程存在時(shí)間優(yōu)化的巨大潛力,并且訴諸 JS 編寫(xiě)的完美 linter 能夠達(dá)到亞秒級(jí)的運(yùn)行時(shí)間。

本期《前端翻譯計(jì)劃》共享的是“加速 JS 生態(tài)系統(tǒng)系列博客”,包括但不限于:

  • PostCSS,SVGO 等等
  • 模塊解析
  • 使用 eslint
  • npm 腳本
  • draft-js emoji 插件
  • polyfill 暴走
  • 桶裝文件崩潰
  • Tailwind CSS

本期共享的是第 2 篇博客 —— eslint。

在本系列的前兩篇文章中,我們已經(jīng)討論了一大坨關(guān)于 linting 的問(wèn)題,所以是時(shí)候讓 eslint 嶄露頭角了。總體而言,eslint 非常靈活,您甚至可以將解析器更換為截然不同的解析器。隨著 JSX 和 TS 的興起,這種情況屢見(jiàn)不鮮。憑借健康的插件和預(yù)設(shè)生態(tài)系統(tǒng)的豐富,每個(gè)用例可能都有一個(gè)規(guī)則,如果沒(méi)有,優(yōu)秀的文檔會(huì)指引您創(chuàng)建自己的規(guī)則。

但這也給性能分析帶來(lái)了一個(gè)問(wèn)題,因?yàn)橛捎趶?qiáng)大的配置靈活性,兩個(gè)項(xiàng)目在 linting 性能方面可能會(huì)有截然不同的體驗(yàn)。

使用 eslint

eslint 的代碼庫(kù)使用任務(wù)運(yùn)行程序抽象來(lái)協(xié)調(diào)常見(jiàn)的構(gòu)建任務(wù),但通過(guò)抽絲剝繭,我們可以拼湊出用于“lint”任務(wù)運(yùn)行的命令,尤其是 JS 文件的 lint。

node bin/eslint.js --report-unused-disable-directives . --ignore-pattern "docs/**"

如你所見(jiàn):ESLint 正在使用 ESLint 檢查自己的代碼庫(kù)!我們將通過(guò) Node 的內(nèi)置 --cpu-prof 參數(shù)生成 *.cpuprofile,并將其加載到 Speedscope 中分析。

通過(guò)將類(lèi)似的調(diào)用堆棧合并在一起,我們可以更清楚地了解時(shí)間開(kāi)銷(xiāo)的“重災(zāi)區(qū)”。這通常被稱(chēng)為“left-heavy”可視化。不要將其與標(biāo)準(zhǔn)火焰圖混淆,火焰圖的 x 軸表示調(diào)用發(fā)生的時(shí)間。相反,這里的 x 軸表示總時(shí)間中消耗的時(shí)間,而不是發(fā)生的時(shí)間。

我們立即可以找出 eslint 代碼庫(kù)中的 linting 設(shè)置花費(fèi)時(shí)間的若干關(guān)鍵區(qū)域。值得注意的是,總時(shí)間的很大一部分花在處理 JSDoc 的規(guī)則上(從函數(shù)名稱(chēng)推斷)。另一個(gè)有趣的方面是,在 lint 任務(wù)期間有兩個(gè)不同的解析器在不同時(shí)間運(yùn)行:esquery 和 acorn。但 JSDoc 規(guī)則花了這么長(zhǎng)時(shí)間,激起了我的好奇心。

一個(gè)特別的 BackwardTokenCommentCursor 入口似乎很有趣,因?yàn)樗窃摻M塊中最大的區(qū)塊。根據(jù)附加的文件定位到源碼,它似乎是一個(gè)保存我們?cè)谖募形恢脿顟B(tài)的類(lèi)。作為第一個(gè)措施,我添加了一個(gè)普通計(jì)數(shù)器,每當(dāng)實(shí)例化該類(lèi)并再次運(yùn)行 lint 任務(wù)時(shí),該計(jì)數(shù)器就會(huì)遞增。

2000 萬(wàn)次

總而言之,該類(lèi)已被構(gòu)造超過(guò) 2000 萬(wàn)次。這看起來(lái)太多了。粉絲請(qǐng)記住,我們實(shí)例化的任何對(duì)象或類(lèi)都會(huì)占用內(nèi)存,并且稍后需要清理該內(nèi)存。我們可以在數(shù)據(jù)中看到垃圾收集(清理內(nèi)存的行為)總共花費(fèi) 2.43 秒的結(jié)果。這并不好。

創(chuàng)建該類(lèi)的新實(shí)例后,它會(huì)調(diào)用兩個(gè)函數(shù),這兩個(gè)函數(shù)似乎都會(huì)啟動(dòng)搜索。如果不了函數(shù)的細(xì)節(jié),那么可以排除第一個(gè)函數(shù),因?yàn)樗话魏涡问降难h(huán)。根據(jù)經(jīng)驗(yàn),循環(huán)通常是研究性能的主要嫌疑人。

第二個(gè)函數(shù)稱(chēng)為 utils.search(),它包含了一個(gè)循環(huán)。它循環(huán)遍歷從我們當(dāng)時(shí)檢查的文件內(nèi)容中解析出的 token 流。token 是編程語(yǔ)言的最小構(gòu)建塊,您可以將它們視為語(yǔ)言的“單詞”。舉個(gè)栗子,在 JS 中,“函數(shù)”一詞通常表示為一個(gè)函數(shù) token,逗號(hào)或單個(gè)分號(hào)也是舉一反一。在 utils.search() 函數(shù)中,我們似乎關(guān)心的是找到距離文件中當(dāng)前位置最近的 token。

exports.search = function search(tokens, location) {
  const index = tokens.findIndex(el => location <= getStartLocation(el))
  return index === -1 ? tokens.length : index
}

為此,搜索是通過(guò) JS 的原生 .findIndex() 方法在 token 數(shù)組上完成的。該算法的說(shuō)明是:

findIndex() 是一種迭代方法。它按升序索引順序?yàn)閿?shù)組中的每個(gè)元素調(diào)用一次提供的 callbackFn 函數(shù),直到 callbackFn 返回真值。

鑒于 token 數(shù)組隨著文件中代碼量的增加而增長(zhǎng),這聽(tīng)起來(lái)并不理想。我們可以使用更有效的算法來(lái)搜索數(shù)組中的值,而不是遍歷數(shù)組中的每個(gè)元素。舉個(gè)栗子,用二分搜索替換那行代碼可以將時(shí)間減少 50%。

雖然減少 50% 看似不錯(cuò),但它仍然沒(méi)有解決此代碼被調(diào)用 2000 萬(wàn)次的問(wèn)題。對(duì)我而言,這才是問(wèn)題所在。我們或多或少地試圖減少癥狀的影響,但是治標(biāo)不治本。我們已經(jīng)在遍歷該文件,因此我們應(yīng)該確切地知道我們?cè)谀睦?。不過(guò),改變這一點(diǎn)需要更具侵入性的重構(gòu),并且對(duì)于本文而言超綱了。看到這不是一個(gè)容易解決的問(wèn)題,我檢查了配置文件中還有哪些值得關(guān)注的內(nèi)容。中心的長(zhǎng)紫色條很難被忽視,不僅因?yàn)樗鼈兊念伾煌?,而且因?yàn)樗鼈冋加昧舜罅繒r(shí)間,并且沒(méi)有深入到數(shù)百個(gè)較小的函數(shù)調(diào)用。

選擇器引擎

speedscope 中的調(diào)用堆棧指向一個(gè)名為 esquery 的項(xiàng)目。這是一個(gè)較舊的項(xiàng)目,其目標(biāo)是能夠通過(guò)小型選擇器語(yǔ)言在解析的代碼中找到特定對(duì)象。如果您仔細(xì)觀察,您會(huì)發(fā)現(xiàn)它與 CSS 選擇器非常相似。它們的套路基本相同,只是我們沒(méi)有在 DOM 樹(shù)中找到特定的 HTML 元素,而是在另一個(gè)樹(shù)結(jié)構(gòu)中找到一個(gè)對(duì)象。

調(diào)試表明 npm 包附帶了壓縮的源碼?;煜淖兞棵ǔJ菃蝹€(gè)字符,強(qiáng)烈暗示壓縮的過(guò)程正在發(fā)生。對(duì)我而言幸運(yùn)的是,該軟件包還附帶了一個(gè)未壓縮的變體,因此我修改了 package.json 來(lái)指向它。稍后再運(yùn)行一次,我們會(huì)得到以下數(shù)據(jù):

好多了!對(duì)于未壓縮的代碼,需要記住的一點(diǎn)是,它的執(zhí)行速度比壓縮的變體慢大約 10-20%。這是一個(gè)粗略的近似范圍,在比較壓縮代碼和未壓縮代碼的性能時(shí),我在整個(gè)職業(yè)生涯中多次測(cè)量過(guò)此范圍。有了這個(gè)經(jīng)驗(yàn),getPath 函數(shù)似乎需要一些幫助。

function getPath(obj, key) {
  var keys = key.split('.')
  var _iterator = _createForOfIteratorHelper(keys),
    _step
  try {
    for (_iterator.s(); !(_step = _iterator.n()).done; ) {
      var _key = _step.value
      if (obj == null) {
        return obj
      }
      obj = obj[_key]
    }
  } catch (err) {
    _iterator.e(err)
  } finally {
    _iterator.f()
  }
  return obj
}

過(guò)時(shí)的轉(zhuǎn)譯將困擾我們很長(zhǎng)一段時(shí)間

如果您已經(jīng)接觸 JS 工具領(lǐng)域一段時(shí)間,那么這些函數(shù)看起來(lái)非常熟悉。_createForOfIteratorHelper 99.99% 是由它們的發(fā)布管道插入的函數(shù),而不是由該庫(kù)的作者插入的。當(dāng) for-of 循環(huán)被添加到 JS 中時(shí),花了一段時(shí)間才普遍支持。

向下轉(zhuǎn)譯現(xiàn)代 JS 功能的工具往往會(huì)因謹(jǐn)慎而犯錯(cuò),并以非常保守的方式重寫(xiě)代碼。在本例中,我們將 string 拆分為字符串?dāng)?shù)組。使用完整的迭代器來(lái)循環(huán)它完全是把飯叫饑,并且一個(gè)無(wú)聊的循環(huán)標(biāo)準(zhǔn)足矣。但由于工具沒(méi)有意識(shí)到這一點(diǎn),因此它們選擇了覆蓋盡可能多場(chǎng)景的變體。以下是用于比較的原始代碼:

function getPath(obj, key) {
  const keys = key.split('.')
  for (const key of keys) {
    if (obj == null) {
      return obj
    }
    obj = obj[key]
  }
  return obj
}

今時(shí)今日,for-of 循環(huán)普遍支持,因此我再次修補(bǔ)了該包,并將函數(shù)實(shí)現(xiàn)替換為源碼中的原始函數(shù)實(shí)現(xiàn)。這一簡(jiǎn)單更改可節(jié)省大約 400 毫秒。我總是對(duì)我們?cè)诶速M(fèi)的 polyfill 或過(guò)時(shí)的向下轉(zhuǎn)譯上消耗了多少 CPU 時(shí)間印象深刻。我還測(cè)量了用標(biāo)準(zhǔn) for 循環(huán)替換 for-of 循環(huán)。

function getPath(obj, key) {
  const keys = key.split('.')
  for (let i = 0; i < keys.length; i++) {
    const key = keys[i]
    if (obj == null) {
      return obj
    }
    obj = obj[key]
  }
  return obj
}

令人驚訝的是,與 for-of 變體相比,這又提高了 200 毫秒。我想即使在今天,for-of 循環(huán)也更難針對(duì)引擎進(jìn)行優(yōu)化。這讓我想起了過(guò)去的一項(xiàng)調(diào)查,Jovi 和我在發(fā)布新版本并切換到 for-of 循環(huán)時(shí),對(duì) graphql 包的解析速度突然變慢進(jìn)行調(diào)查。

這是 v8/gecko/webkit 工程師可以正確驗(yàn)證的東東,但我的假設(shè)是它仍然必須調(diào)用迭代器協(xié)議,因?yàn)檫@可能已被全局覆蓋,這將改變每個(gè)數(shù)組的行為。大抵就是如此吧。

雖然我們從這些變化中快速斬獲了一些成果,但仍遠(yuǎn)未達(dá)到理想狀態(tài)。總體而言,該功能仍然是有待優(yōu)化的首要競(jìng)爭(zhēng)者,因?yàn)樗鼏为?dú)負(fù)責(zé)了總時(shí)間的幾秒鐘。再次應(yīng)用快速計(jì)數(shù)器的奇技淫巧,發(fā)現(xiàn)它被調(diào)用了大約 22k 次。可以肯定的是,這個(gè)函數(shù)在某種程度上處于“hot”路徑中。

特別值得注意的是,一大坨處理字符串的性能密集型代碼都圍繞 String.prototype.split() 方法。這將有效地迭代所有字符,分配一個(gè)新數(shù)組,然后迭代該數(shù)組,所有這些都可以在一次迭代中完成。

function getPath(obj, key) {
  let last = 0
  // 有效,因?yàn)樗械逆I都是 ASCII,而不是 unicode
  for (let i = 0; i < key.length; i++) {
    if (obj == null) {
      return obj
    }
    if (key[i] === '.') {
      obj = obj[key.slice(last, i)]
      last = i + 1
    } else if (i === key.length - 1) {
      obj = obj[key.slice(last)]
    }
  }
  return obj
}

這次重寫(xiě)對(duì)其性能影響巨大。當(dāng)我們開(kāi)始時(shí),getPath 總共花費(fèi)了 2.7 秒,應(yīng)用了所有優(yōu)化后,我們?cè)O(shè)法將其降低到 486 毫秒。

繼續(xù)使用 matches() 函數(shù),我們看到奇怪的 for-of 向下轉(zhuǎn)譯產(chǎn)生了大量開(kāi)銷(xiāo)。為了節(jié)省時(shí)間,我直接在 github 上復(fù)制了源碼中的函數(shù)。由于 matches() 在調(diào)試中更加突兀,因此僅此更改就節(jié)省了整整 1 秒。

我們生態(tài)系統(tǒng)中的一大坨庫(kù)都面臨此問(wèn)題。我真的希望有一種銀彈可以一鍵更新它們。也許我們需要一個(gè)反向轉(zhuǎn)譯器來(lái)檢測(cè)向下轉(zhuǎn)譯模式,并將其再次轉(zhuǎn)換回現(xiàn)代代碼。

我聯(lián)系了 jviide,看看是否可以進(jìn)一步優(yōu)化 matches()。通過(guò)其額外更改,我們能夠使整個(gè)選擇器代碼比原始未修改狀態(tài)快大約 5 倍。它基本上所做的就是消除 matches() 函數(shù)中的大量開(kāi)銷(xiāo),這也使它能夠簡(jiǎn)化一些相關(guān)的輔助函數(shù)。舉個(gè)栗子,它注意到模板字符串的轉(zhuǎn)譯效果很差。

// 輸入
const literal = `${selector.value.value}`

// 輸出,向下轉(zhuǎn)譯很慢
const literal = ''.concat(selector.value.value)

它甚至更進(jìn)一步,將每個(gè)新選擇器解析為動(dòng)態(tài)函數(shù)調(diào)用鏈,并緩存生成的包裝函數(shù)。這個(gè)技巧再次大幅加速了選擇器引擎。

提早紓困

有時(shí)退后一步,從不同的角度解決問(wèn)題是件好事。到目前為止,我們已經(jīng)了解了實(shí)現(xiàn)細(xì)節(jié),但我們實(shí)際上正在處理什么樣的選擇器?是否有可能使其中一些短路?為了測(cè)試這個(gè)理論,我首先需要更好地了解正在處理的選擇器類(lèi)型。毫不奇怪,大多數(shù)選擇器都很短。其中有幾個(gè)確實(shí)很有特色。舉個(gè)栗子,這是一個(gè)簡(jiǎn)單選擇器:

VariableDeclaration:not(ExportNamedDeclaration > .declaration) > VariableDeclarator.declarations:matches(
  [init.type="ArrayExpression"],
  :matches(
 [init.type="CallExpression"],
[init.type="NewExpression"]
  )[init.optional!=true][init.callee.type="Identifier"][init.callee.name="Array"],
[init.type="CallExpression"][init.optional!=true][init.callee.type="MemberExpression"][init.callee.computed!=true][init.callee.property.type="Identifier"][init.callee.optional!=true]
 :matches(
   [init.callee.property.name="from"],
   [init.callee.property.name="of"]
)[init.callee.object.type="Identifier"][init.callee.object.name="Array"],
[init.type="CallExpression"][init.optional!=true][init.callee.type="MemberExpression"][init.callee.computed!=true][init.callee.property.type="Identifier"][init.callee.optional!=true]:matches(
   [init.callee.property.name="concat"],
   [init.callee.property.name="copyWithin"],
   [init.callee.property.name="fill"],
   [init.callee.property.name="filter"],
   [init.callee.property.name="flat"],
   [init.callee.property.name="flatMap"],
   [init.callee.property.name="map"],
   [init.callee.property.name="reverse"],
   [init.callee.property.name="slice"],
   [init.callee.property.name="sort"],
   [init.callee.property.name="splice"]
 )
  ) > Identifier.id

這無(wú)疑是一個(gè)有點(diǎn)偏離軌道的例子。我不想成為那個(gè)在不正確匹配時(shí)必須進(jìn)行調(diào)試的倒霉蛋。這是我對(duì)任何形式的自定義領(lǐng)域特定語(yǔ)言的主要抱怨。它們通常根本不提供工具支持。如果我們留在 JS 領(lǐng)域,我們可以使用適當(dāng)?shù)恼{(diào)試器隨時(shí)隨地檢查該值。雖然前面的字符串選擇器示例有點(diǎn)極端,但大多數(shù)選擇器如下所示:

BinaryExpression

/* 或者 */
VariableDeclaration

僅此而已。大多數(shù)選擇器只是想知道當(dāng)前 AST 節(jié)點(diǎn)是否屬于某種類(lèi)型。為此,我們實(shí)際上并不需要整個(gè)選擇器引擎。如果我們?yōu)榇艘胍粭l捷徑,并完全繞過(guò)選擇器引擎會(huì)怎么樣?

class NodeEventGenerator {
  // ...
  isType = new Set([
    'IfStatement',
    'BinaryExpression'
    // 其他......
  ])
  applySelector(node, selector) {
    // 捷徑,只需斷言類(lèi)型
    if (this.isType.has(selector.rawSelector)) {
      if (node.type === selector.rawSelector) {
        this.emitter.emit(selector.rawSelector, node)
      }
      return
    }
    // 回退到完整的選擇器引擎匹配
    if (
      esquery.matches(
        node,
        selector.parsedSelector,
        this.currentAncestry,
        this.esqueryOptions
      )
    ) {
      this.emitter.emit(selector.rawSelector, node)
    }
  }
}

由于我們已經(jīng)短路了選擇器引擎,我開(kāi)始好奇字符串化選擇器與以純 JS 函數(shù)編寫(xiě)的選擇器相比如何。我的直覺(jué)告訴我,將選擇器編寫(xiě)為簡(jiǎn)單的 JS 條件會(huì)更容易針對(duì)引擎優(yōu)化。

反思選擇器

如果您需要像我們?cè)跒g覽器中使用 CSS 那樣跨越語(yǔ)言障礙傳遞遍歷命令,那么選擇器引擎非常有用。但它從來(lái)都不是免費(fèi)的,因?yàn)檫x擇器引擎總是需要解析選擇器來(lái)解構(gòu)我們應(yīng)該做什么,然后動(dòng)態(tài)構(gòu)建一些邏輯來(lái)執(zhí)行解析的東西。

但在 eslint 內(nèi)部我們沒(méi)有跨越任何語(yǔ)言障礙。我們還停留在 JS 領(lǐng)域。因此,通過(guò)將查詢指令轉(zhuǎn)換為選擇器,并將它們解析回我們可以再次運(yùn)行的內(nèi)容,我們不會(huì)獲得任何性能方面的好處。相反,我們消耗了大約 25% 的總 linting 時(shí)間來(lái)解析和執(zhí)行選擇器。我們需要一種新方案。

從概念上講,選擇器只不過(guò)是一個(gè)“描述”,用于根據(jù)它所持有的條件來(lái)查找元素。這可能是在樹(shù)或平面數(shù)據(jù)結(jié)構(gòu)(比如 array)中的查找。如果您考慮一下,即使標(biāo)準(zhǔn) Array.prototype.filter() 調(diào)用中的回調(diào)函數(shù)也是一個(gè)選擇器。我們從元素集合(數(shù)組)中選擇值,并僅選擇我們關(guān)心的值。我們對(duì) esquery 所做的事情完全相同。從一堆對(duì)象(AST 節(jié)點(diǎn))中,我們挑選出符合特定條件的對(duì)象。那就是一個(gè)選擇器!那么,如果我們避免選擇器解析邏輯,并使用純 JS 函數(shù)呢?

// String 筑基的 esquery 選擇器
const esquerySelector = `[type="CallExpression"][callee.type="MemberExpression"][callee.computed!=true][callee.property.type="Identifier"]:matches([callee.property.name="substr"], [callee.property.name="substring"])`
// 純 JS 函數(shù)的同款選擇器
function jsSelector(node) {
  return (
    node.type === 'CallExpression' &&
    node.callee.type === 'MemberExpression' &&
    !node.callee.computed &&
    node.callee.property.type === 'Identifier' &&
    (node.callee.property.name === 'substr' ||
      node.callee.property.name === 'substring')
  )
}

讓我們嘗試一下吧!我編寫(xiě)了一些基準(zhǔn)來(lái)衡量這兩種方案的時(shí)間差異。

whatfoo.substr(1, 2) ops/sec
esquery422,848.208
esquery(優(yōu)化)3,036,384.255
純 JS 函數(shù)66,961,066.5239

看起來(lái)純 JS 函數(shù)變體對(duì)基于字符串的函數(shù)變體“降維打擊”。這簡(jiǎn)直棒棒噠。即使在花費(fèi)了所有時(shí)間來(lái)使 esquery 更快之后,它仍遠(yuǎn)不及 JS 變體。在選擇器不匹配,且引擎可以提前退出的情況下,它仍然比普通函數(shù)慢 30 倍。這個(gè)小實(shí)驗(yàn)證實(shí)了我的假設(shè),即我們?yōu)檫x擇器引擎付出了相當(dāng)多的時(shí)間。

第三方插件和預(yù)設(shè)的影響

盡管在 eslint 設(shè)置的配置文件中可以看到更多的優(yōu)化空間,但我開(kāi)始懷疑我是否花時(shí)間優(yōu)化了正確的事情。到目前為止,我們?cè)?eslint 自己的 linting 設(shè)置中看到的相同問(wèn)題是否也出現(xiàn)在其他 linting 設(shè)置中?eslint 的主要優(yōu)勢(shì)之一始終是其靈活性和對(duì)第三方 linting 規(guī)則的支持?;仡欉^(guò)去,我從事的幾乎每個(gè)項(xiàng)目都安裝了一些自定義 linting 規(guī)則和大約 2-5 個(gè)額外的 eslint 插件或預(yù)設(shè)。但更重要的是,它們完全關(guān)閉了解析器??焖贋g覽一下 npm 下載統(tǒng)計(jì)數(shù)據(jù),就可以發(fā)現(xiàn)替換 eslint 內(nèi)置解析器的趨勢(shì):

軟件包npm 周下載量%
eslint31.718.905100%
@typescript-eslint/parser23.192.76773%
@babel/eslint-parser6.057.11019%

如果這些數(shù)字可信,那就意味著,所有 eslint 用戶中只有 8% 使用內(nèi)置解析器。它還顯示了 TS 已經(jīng)變得多么普遍,占 eslint 總用戶群的最大份額(73%)。我們沒(méi)有關(guān)于 babel 解析器的用戶是否也將其用于 TS 的數(shù)據(jù)。我的猜測(cè)是,它們中的一部分人這樣做了,而且 TS 用戶的總數(shù)實(shí)際上甚至更高。

在分析各種開(kāi)源存儲(chǔ)庫(kù)中的若干不同設(shè)置后,我選擇了 Vite 中的一個(gè),它也包含其他配置文件中存在的大量模式。它的代碼庫(kù)是用 TS 編寫(xiě)的,并且 eslint 的解析器已相應(yīng)替換。

和之前一樣,我們可以找出各個(gè)區(qū)域,顯示時(shí)間花在哪里。有一個(gè)區(qū)域暗示了,從 TS 格式到 eslint 格式的轉(zhuǎn)換需要相當(dāng)多的時(shí)間。配置加載也發(fā)生了一些怪事,因?yàn)樗肋h(yuǎn)不會(huì)像這里那樣占用那么多時(shí)間。我們找到了一個(gè)老朋友,eslint-import-plugin 和 eslint-plugin-node,它們似乎啟動(dòng)了一堆模塊解析邏輯。

不過(guò),這里有趣的一點(diǎn)是,選擇器引擎的開(kāi)銷(xiāo)并未顯示。有一些 applySelector 函數(shù)被調(diào)用的實(shí)例,但從整體上看它幾乎不消耗任何時(shí)間。

eslint-plugin-import 和 eslint-plugin-node 似乎總是彈出并需要相當(dāng)長(zhǎng)的時(shí)間才能執(zhí)行的兩個(gè)第三方插件。每當(dāng)這些插件之一或兩個(gè)處于活動(dòng)狀態(tài)時(shí),它就會(huì)真正顯示在分析數(shù)據(jù)中。兩者都會(huì)導(dǎo)致大量的文件系統(tǒng)流量,因?yàn)樗鼈儑L試解析一堆模塊,但不緩存結(jié)果。

轉(zhuǎn)換所有 AST 節(jié)點(diǎn)

我們將從起初發(fā)生的 TS 轉(zhuǎn)換開(kāi)始。我們的工具將提供給它們的代碼解析為一種稱(chēng)為 AST(抽象語(yǔ)法樹(shù))的數(shù)據(jù)結(jié)構(gòu)。您可以將其視為我們所有工具所使用的構(gòu)建塊。它告訴的信息如下:“瞧,這里我們聲明一個(gè)變量,它有這個(gè)名稱(chēng)和那個(gè)值”,或者“這里有一個(gè)帶有這個(gè)條件的 if 語(yǔ)句,它保護(hù)那個(gè)代碼塊”,等等。

// `const foo = 42` 的 AST 形式如下所示:
{
  type: "VariableDeclaration",
  kind: "const",
  declarations: [
    {
      kind: "VariableDeclarator",
      name: {
        type: "Identifier",
        name: "foo",
      },
      init: {
        type: "NumericLiteral",
        value: 42
      }
  ]
}

您可以在優(yōu)秀的 AST Explorer 網(wǎng)站上親眼看到我們的工具如何解析代碼。它可以讓您更好地了解我們工具的 AST 格式的異同點(diǎn)。

雖然但是,在 eslint 的情況下存在問(wèn)題。無(wú)論我們選擇什么解析器,我們都希望規(guī)則能夠正常工作。當(dāng)我們激活 no-console 規(guī)則時(shí),我們希望它適用于所有規(guī)則,而不是強(qiáng)制為每個(gè)解析器重寫(xiě)每個(gè)規(guī)則。本質(zhì)上,我們需要的是一個(gè)我們都同意的共享 AST 格式。這正是 eslint 所做的。它期望每個(gè) AST 節(jié)點(diǎn)都匹配 estree 規(guī)范,該規(guī)范規(guī)定了每個(gè) AST 節(jié)點(diǎn)的外觀。這個(gè)規(guī)范存在已久,一大坨 JS 工具都始于該規(guī)范。甚至 babel 也構(gòu)建于其上,但此后有若干記錄在案的偏差。

但當(dāng)您使用 TS 時(shí),問(wèn)題的癥結(jié)就在這里。TS 的 AST 格式非常不同,因?yàn)樗€需要考慮代表類(lèi)型本身的節(jié)點(diǎn)。一些構(gòu)造在內(nèi)部也有不同的表示,因?yàn)樗?TS 本身變得更容易。這意味著,每個(gè) TS AST 節(jié)點(diǎn)都必須轉(zhuǎn)換為 eslint 格式。這種轉(zhuǎn)換需要時(shí)間。在此配置文件中,約占總時(shí)間的 22%?;ㄙM(fèi)這么長(zhǎng)時(shí)間的原因不僅僅是遍歷本身,而且每次轉(zhuǎn)換我們都會(huì)分配新的對(duì)象。我們?cè)趦?nèi)存中基本上有兩個(gè)不同 AST 格式的副本。

也許 babel 的解析器更快?如果我們用 @babel/eslint-parser 替換 @typescript-eslint/parser,那又如何?

what解析時(shí)間
@typescript-eslint/parser2.1s
@babel/eslint-parser + @babel/preset-typescript0.6s

事實(shí)證明,這樣做可以節(jié)省相當(dāng)多的時(shí)間。有趣的是,這一更改還大大縮短了配置加載時(shí)間。配置加載時(shí)間的改進(jìn)可能是由于 babel 的解析器分布在更少的文件中。

粉絲請(qǐng)注意,雖然 babel 解析器明顯更快,但它不支持類(lèi)型感知 linting。這是 @typescript-eslint/parser 獨(dú)有的功能。這為諸如 no-for-in-array 規(guī)則之類(lèi)的規(guī)則提供了可能性,它可以檢測(cè)您在 for-in 循環(huán)中迭代的變量實(shí)際上是否是 object 的 array。因此您可能想繼續(xù)使用 @typescript-eslint/parser。如果您確信您不使用它們的任何規(guī)則,并且您只是需要 eslint 來(lái)理解 TS 的語(yǔ)法,再加上更快一點(diǎn)的 lint,那么切換到 babel 的解析器是一個(gè)不錯(cuò)的選擇。

理想的 linter 是什么樣子?

我偶然發(fā)現(xiàn)了關(guān)于 eslint 未來(lái)的討論,其中性能是首要任務(wù)之一。其中提出了一些很棒的想法,特別是引入會(huì)話的概念,這允許完整的程序檢查,而不是像今天那樣在每個(gè)文件的基礎(chǔ)上進(jìn)行檢查。鑒于至少 73% 的 eslint 用戶使用它來(lái)檢查 TS 代碼,因此需要更少 AST 轉(zhuǎn)換的更緊密集成也會(huì)對(duì)性能產(chǎn)生巨大影響。

還有一些關(guān)于 Rust 移植的討論,這激起了我對(duì)當(dāng)前基于 Rust 的 JS linter 的速度有多快的好奇。rslint 是唯一一個(gè)似乎已經(jīng)做好生產(chǎn)準(zhǔn)備,并能夠解析大部分 TS 語(yǔ)法的工具。

除了 rslint,我還開(kāi)始想知道純 JS 中的簡(jiǎn)單 linter 會(huì)是什么樣子。一種沒(méi)有選擇器引擎,不需要持續(xù)的 AST 轉(zhuǎn)換,只需要解析代碼并檢查其上的各種規(guī)則。所以我用一個(gè)非常簡(jiǎn)單的 API 包裝了 babel 的解析器,并添加了自定義遍歷邏輯來(lái)遍歷 AST 樹(shù)。我沒(méi)有選擇 babel 自己的遍歷函數(shù),因?yàn)樗鼈冊(cè)诿看蔚鷷r(shí)都會(huì)導(dǎo)致大量分配,并且是基于生成器構(gòu)建的,這比不使用生成器要慢一些。還嘗試了一些我自己多年來(lái)編寫(xiě)的自定義 JS/TS 解析器,這些解析器源于幾年前將 esbuild 的解析器移植到 JS。

話雖如此,以下是在 Vite 存儲(chǔ)庫(kù)上運(yùn)行它們時(shí)的數(shù)字(144 個(gè)文件)。

what時(shí)間
eslint(JS)5.85s
自定義 linter(JS)0.52s
rslint(Rust 筑基)0.45s

基于這些數(shù)字,我相當(dāng)有信心,基于這個(gè)小實(shí)驗(yàn),我們只需使用 JS 就可以非常接近 Rust 的性能。

完美謝幕

總的來(lái)說(shuō),eslint 項(xiàng)目有著非常光明的前景。它是最成功的 OSS 項(xiàng)目之一,并且找到了獲得大量資金的秘訣。我們研究了一些能讓 eslint 更快的東西,還有一大坨這里沒(méi)有涉及的領(lǐng)域需要研究。

“eslint 的未來(lái)”討論包含了一大坨偉大的想法,這些想法將使 eslint 變得更好且可能更快。我認(rèn)為頭大的一點(diǎn)是,避免嘗試立即解決所有問(wèn)題,因?yàn)楦鶕?jù)我的經(jīng)驗(yàn),這通常注定會(huì)失敗。徹底重寫(xiě)也是如此。相反,我認(rèn)為當(dāng)前的代碼庫(kù)是一個(gè)完美的起點(diǎn),可以被塑造成更棒的東西。

從局外人的角度來(lái)看,需要做出一些關(guān)鍵決定。舉個(gè)栗子,此時(shí)繼續(xù)支持基于字符串的選擇器是否有意義?如果是,eslint 團(tuán)隊(duì)是否有能力承擔(dān) esquery 的維護(hù)工作,并給予它一些急需的關(guān)愛(ài)?鑒于 npm 下載計(jì)數(shù)表明 73% 的 eslint 用戶是 TS 用戶,那么原生 TS 支持又如何呢?

免責(zé)聲明

本文屬于是語(yǔ)冰的直男翻譯了屬于是,略有刪改,僅供粉絲參考,英文原味版請(qǐng)傳送 Speeding up the JavaScript ecosystem - eslint

以上就是JS生態(tài)系統(tǒng)加速eslint解析器使用實(shí)例探索的詳細(xì)內(nèi)容,更多關(guān)于JS eslint解析器的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!

相關(guān)文章

最新評(píng)論