欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

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

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

引言

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

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

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

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

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

但這也給性能分析帶來了一個問題,因為由于強大的配置靈活性,兩個項目在 linting 性能方面可能會有截然不同的體驗。

使用 eslint

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

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

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

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

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

一個特別的 BackwardTokenCommentCursor 入口似乎很有趣,因為它是該組塊中最大的區(qū)塊。根據(jù)附加的文件定位到源碼,它似乎是一個保存我們在文件中位置狀態(tài)的類。作為第一個措施,我添加了一個普通計數(shù)器,每當(dāng)實例化該類并再次運行 lint 任務(wù)時,該計數(shù)器就會遞增。

2000 萬次

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

創(chuàng)建該類的新實例后,它會調(diào)用兩個函數(shù),這兩個函數(shù)似乎都會啟動搜索。如果不了函數(shù)的細(xì)節(jié),那么可以排除第一個函數(shù),因為它不包含任何形式的循環(huán)。根據(jù)經(jīng)驗,循環(huán)通常是研究性能的主要嫌疑人。

第二個函數(shù)稱為 utils.search(),它包含了一個循環(huán)。它循環(huán)遍歷從我們當(dāng)時檢查的文件內(nèi)容中解析出的 token 流。token 是編程語言的最小構(gòu)建塊,您可以將它們視為語言的“單詞”。舉個栗子,在 JS 中,“函數(shù)”一詞通常表示為一個函數(shù) token,逗號或單個分號也是舉一反一。在 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
}

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

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

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

雖然減少 50% 看似不錯,但它仍然沒有解決此代碼被調(diào)用 2000 萬次的問題。對我而言,這才是問題所在。我們或多或少地試圖減少癥狀的影響,但是治標(biāo)不治本。我們已經(jīng)在遍歷該文件,因此我們應(yīng)該確切地知道我們在哪里。不過,改變這一點需要更具侵入性的重構(gòu),并且對于本文而言超綱了??吹竭@不是一個容易解決的問題,我檢查了配置文件中還有哪些值得關(guān)注的內(nèi)容。中心的長紫色條很難被忽視,不僅因為它們的顏色不同,而且因為它們占用了大量時間,并且沒有深入到數(shù)百個較小的函數(shù)調(diào)用。

選擇器引擎

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

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

好多了!對于未壓縮的代碼,需要記住的一點是,它的執(zhí)行速度比壓縮的變體慢大約 10-20%。這是一個粗略的近似范圍,在比較壓縮代碼和未壓縮代碼的性能時,我在整個職業(yè)生涯中多次測量過此范圍。有了這個經(jīng)驗,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
}

過時的轉(zhuǎn)譯將困擾我們很長一段時間

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

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

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

今時今日,for-of 循環(huán)普遍支持,因此我再次修補了該包,并將函數(shù)實現(xiàn)替換為源碼中的原始函數(shù)實現(xiàn)。這一簡單更改可節(jié)省大約 400 毫秒。我總是對我們在浪費的 polyfill 或過時的向下轉(zhuǎn)譯上消耗了多少 CPU 時間印象深刻。我還測量了用標(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)也更難針對引擎進(jìn)行優(yōu)化。這讓我想起了過去的一項調(diào)查,Jovi 和我在發(fā)布新版本并切換到 for-of 循環(huán)時,對 graphql 包的解析速度突然變慢進(jìn)行調(diào)查。

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

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

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

function getPath(obj, key) {
  let last = 0
  // 有效,因為所有的鍵都是 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
}

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

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

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

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

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

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

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

提早紓困

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

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

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

BinaryExpression

/* 或者 */
VariableDeclaration

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

class NodeEventGenerator {
  // ...
  isType = new Set([
    'IfStatement',
    'BinaryExpression'
    // 其他......
  ])
  applySelector(node, selector) {
    // 捷徑,只需斷言類型
    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)短路了選擇器引擎,我開始好奇字符串化選擇器與以純 JS 函數(shù)編寫的選擇器相比如何。我的直覺告訴我,將選擇器編寫為簡單的 JS 條件會更容易針對引擎優(yōu)化。

反思選擇器

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// `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 格式的異同點。

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

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

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

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

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

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

理想的 linter 是什么樣子?

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

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

除了 rslint,我還開始想知道純 JS 中的簡單 linter 會是什么樣子。一種沒有選擇器引擎,不需要持續(xù)的 AST 轉(zhuǎn)換,只需要解析代碼并檢查其上的各種規(guī)則。所以我用一個非常簡單的 API 包裝了 babel 的解析器,并添加了自定義遍歷邏輯來遍歷 AST 樹。我沒有選擇 babel 自己的遍歷函數(shù),因為它們在每次迭代時都會導(dǎo)致大量分配,并且是基于生成器構(gòu)建的,這比不使用生成器要慢一些。還嘗試了一些我自己多年來編寫的自定義 JS/TS 解析器,這些解析器源于幾年前將 esbuild 的解析器移植到 JS。

話雖如此,以下是在 Vite 存儲庫上運行它們時的數(shù)字(144 個文件)。

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

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

完美謝幕

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

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

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

免責(zé)聲明

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

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

相關(guān)文章

最新評論