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

JS生態(tài)系統(tǒng)加速一次一庫PostCSS SVGO的重構(gòu)源碼和性能優(yōu)化探索

 更新時(shí)間:2024年01月21日 14:40:11   作者:大家的林語冰 人貓神話  
這篇文章主要介紹了JS生態(tài)系統(tǒng)加速一次一庫PostCSS SVGO的重構(gòu)源碼和性能優(yōu)化探索,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪

引言

長話短說:大多數(shù)流行庫可以通過避免不必要的類型轉(zhuǎn)換,或避免在函數(shù)內(nèi)創(chuàng)建函數(shù)來優(yōu)化。

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

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

本期共享的是第一篇博客 —— 一次一庫的重構(gòu)源碼和性能優(yōu)化。

雖然前端趨勢似乎是用 Rust 或 Go 等其他語言重寫 JS 構(gòu)建工具,但目前 JS 筑基的工具可能足夠快。典型前端項(xiàng)目中的構(gòu)建管道通常由一大坨協(xié)同工作的不同工具組成。但工具的多樣化使得工具維護(hù)者難以發(fā)現(xiàn)性能瓶頸,因?yàn)樗鼈冃枰雷约旱墓ぞ呤褂昧四男┢渌ぞ摺?/p>

盡管從純語言的角度來看,JS 肯定比 Rust 或 Go 慢,但目前 JS 筑基的工具還有優(yōu)化空間。當(dāng)然,JS 速度較慢,但與現(xiàn)在相比,它不至于太慢。JIT 引擎現(xiàn)在就快得要命!

好奇心引導(dǎo)我費(fèi)時(shí)分析常見的 JS 筑基的工具,了解其性能開銷之所在。讓我們從 PostCSS 開始,它是一個(gè)人氣爆棚的 CSS 解析器和轉(zhuǎn)譯器。

在 PostCSS 中優(yōu)化 4.6 秒

有一個(gè)神通廣大的插件,名為 postcss-custom-properties,它在舊版瀏覽器中添加了 CSS 自定義屬性的基本支持。不知為何,它在調(diào)試中非常扎眼,昂貴的 4.6 秒歸因于其內(nèi)部使用的簡單正則。這有點(diǎn)奇葩。

正則表達(dá)式目測是疑似搜索特定注釋值,更改插件行為的東東,類似于 eslint 中用于禁用特定 linting 規(guī)則的東東。它們的 README 中沒有提及這一點(diǎn),但偷瞄源碼證實(shí)了此猜想。

創(chuàng)建正則表達(dá)式的位置是檢查 CSS 規(guī)則或聲明前面是否有所述注釋的函數(shù)的一部分。

function isBlockIgnored(ruleOrDeclaration) {
  const rule = ruleOrDeclaration.selector
    ? ruleOrDeclaration
    : ruleOrDeclaration.parent

  return /(!\s*)?postcss-custom-properties:\s*off\b/i.test(rule.toString())
}

rule.toString() 調(diào)用瞬間引起了我的注意。如果您要解決性能問題,那么將一種類型轉(zhuǎn)換為另一種類型的地方通常值得三思而行,因?yàn)檗D(zhuǎn)換總會有時(shí)間成本。此場景中有趣的是,rule 變量始終持有一個(gè)攜帶自定義 toString 方法的 object。該變量一開始就不是一個(gè)字符串,所以我們在這里付出一些序列化開銷,才能測試正則表達(dá)式。根據(jù)個(gè)人經(jīng)驗(yàn),我知道將正則表達(dá)式與一大坨短字符串匹配,比與一小坨長字符串匹配要慢得多。此處有待優(yōu)化!

這段代碼相當(dāng)麻煩的一點(diǎn)是,無論文件是否有 postcss 注釋,每個(gè)輸入文件都有此開銷。知道在長字符串上運(yùn)行一個(gè)正則表達(dá)式比在短字符串上運(yùn)行重復(fù)的正則表達(dá)式和序列化成本更低,我們可以驗(yàn)證此函數(shù),避免在知道文件不包含任何 postcss 注釋時(shí),也調(diào)用 isBlockIgnored。

應(yīng)用修復(fù)后,構(gòu)建時(shí)間縮短了 4.6 秒!

優(yōu)化 SVG 壓縮速度

接下來是 SVGO,一個(gè)用于壓縮 SVG 文件的庫。它功能強(qiáng)大,并且是具有大量 SVG 圖標(biāo)的項(xiàng)目的主要工具。CPU 配置文件顯示,壓縮 SVG 花費(fèi)了 3.1 秒。我們能提速嗎?

在分析數(shù)據(jù)中搜索一下,有一個(gè)函數(shù)很突兀:strongRound。更重要的是,該函數(shù)之后總是會進(jìn)行一些 GC 清理。

讓我們在 GitHub 上獲取源碼:

/**
 * 降低路徑數(shù)據(jù)中浮點(diǎn)數(shù)的精度
 * 保持指定數(shù)量的小數(shù)。
 * 智能四舍五入:比如 2.3491 取舍為 2.35 而不是 2.349
 */
function strongRound(data: number[]) {
 for (var i = data.length; i-- > 0; ) {
  if (data[i].toFixed(precision) != data[i]) {
   var rounded = +data[i].toFixed(precision - 1);
   data[i] =
    +Math.abs(rounded - data[i]).toFixed(precision + 1) >= error
     ? +data[i].toFixed(precision)
     : rounded;
  }
 }
 return data;
}

所以這是一個(gè)用于壓縮數(shù)字的函數(shù),在任何典型的 SVG 文件中都有一大坨類似的函數(shù)。該函數(shù)接收 numbers 數(shù)組,并預(yù)計(jì)會改變其元素。讓我們瞄一下其實(shí)現(xiàn)中使用的變量類型。通過仔細(xì)檢查,我們發(fā)現(xiàn)字符串和數(shù)字之間存在一大坨來回轉(zhuǎn)換。

function strongRound(data: number[]) {
 for (var i = data.length; i-- > 0; ) {
  // string 和 number 比較 -> string 轉(zhuǎn)換為 number
  if (data[i].toFixed(precision) != data[i]) {
   // 基于 number 創(chuàng)建 string,然后立刻轉(zhuǎn)換為 number
   var rounded = +data[i].toFixed(precision - 1);
   data[i] =
    // number 轉(zhuǎn) string,然后直接轉(zhuǎn)換為 number
    +Math.abs(rounded - data[i]).toFixed(precision + 1) >= error
     ? // 這和之前的 if 條件的值相同
       // 只是再次轉(zhuǎn)換為 number
       +data[i].toFixed(precision)
     : rounded;
  }
 }
 return data;
}

對數(shù)字進(jìn)行四舍五入的操作似乎只需一點(diǎn)點(diǎn)數(shù)學(xué)就歐了,而無需將數(shù)字轉(zhuǎn)換為字符串。作為一般經(jīng)驗(yàn)法則,大部分優(yōu)化都是用數(shù)字來表達(dá)事物,主要原因是 CPU 非常擅長處理數(shù)字。通過若干更改,我們可以確保我們始終保持在數(shù)字范圍內(nèi),完全避免字符串轉(zhuǎn)換。

// 類似 Number.prototype.toFixed 的功能
// 但返回值不轉(zhuǎn)換為 string
function toFixed(num, precision) {
 const pow = 10 ** precision;
 return Math.round(num * pow) / pow;
}

// 重寫避免 string 轉(zhuǎn)換
// 調(diào)用我們自己的 toFixed() 函數(shù)
function strongRound(data: number[]) {
 for (let i = data.length; i-- > 0; ) {
  const fixed = toFixed(data[i], precision);
  // 我們可以使用嚴(yán)格相等比較
  if (fixed !== data[i]) {
   const rounded = toFixed(data[i], precision - 1);
   data[i] =
    toFixed(Math.abs(rounded - data[i]), precision + 1) >= error
     ? fixed // 現(xiàn)在這里我們可以復(fù)用之前的值
     : rounded;
  }
 }
 return data;
}

再次運(yùn)行分析證實(shí),我們能夠?qū)?gòu)建時(shí)間加快約 1.4 秒!

短字符串上的正則

在 strongRound 附近的另一個(gè)函數(shù)看起來很可疑,因?yàn)樗ㄙM(fèi)了約 1 秒(0.9 秒)才完成。

與 stringRound 類似,此函數(shù)用于壓縮數(shù)字,但增加了一個(gè)技巧,如果數(shù)字有小數(shù)且小于 1 和大于 -1,我們可以刪除前導(dǎo)零。所以 0.5 可以壓縮為 .5,-0.2 則是 -.2。特別是最后一行看起來很有趣。

const stringifyNumber = (number: number, precision: number) => {
 // 從十進(jìn)制數(shù)中刪除零整數(shù)
 return number.toString().replace(/^0\./, ".").replace(/^-0\./, "-.");
};

在這里,我們將數(shù)字轉(zhuǎn)換為字符串,并對其調(diào)用正則表達(dá)式。該數(shù)字的字符串版本很可能是一個(gè)短字符串。而且我們知道一個(gè)數(shù)字不能同時(shí)是 n > 0 && n < 1 和 n > -1 && n < 0。連 NaN 無法如此逆天!由此我們可以推斷,要么有且僅有一個(gè)正則表達(dá)式匹配,要么沒有正則表達(dá)式匹配,但絕不會兩者都匹配。至少有一個(gè) .replace 調(diào)用總被浪費(fèi)。

我們可以通過手動(dòng)區(qū)分這些情況來優(yōu)化它。當(dāng)且僅當(dāng)我們正在處理一個(gè)具有前導(dǎo) 0 的數(shù)字時(shí),我們才應(yīng)用替換邏輯。這些數(shù)字檢查比執(zhí)行正則表達(dá)式搜索更快。

const stringifyNumber = (number: number, precision: number) => {
  // 從十進(jìn)制數(shù)中刪除零整數(shù)
 const strNum = number.toString();
 // 使用簡單數(shù)字檢驗(yàn)
 if (0 < num && num < 1) {
  return strNum.replace(/^0\./, ".");
 } else if (-1 < num && num < 0) {
  return strNum.replace(/^-0\./, "-.");
 }
 return strNum;
};

我們可以更進(jìn)一步,完全擺脫正則表達(dá)式搜索,因?yàn)槲覀?100% 確定前導(dǎo) 0 位于字符串中,因此可以直接操作字符串。

const stringifyNumber = (number: number, precision: number) => {
  // 從十進(jìn)制數(shù)中刪除零整數(shù)
 const strNum = number.toString();
 if (0 < num && num < 1) {
  // 我們只需要簡單的字符串處理
  return strNum.slice(1);
 } else if (-1 < num && num < 0) {
  // 我們只需要簡單的字符串處理
  return "-" + strNum.slice(2);
 }
 return strNum;
};

由于 svgo 的代碼庫中已經(jīng)有一個(gè)單獨(dú)的函數(shù)可以移除前導(dǎo) 0,因此我們可以利用它。又節(jié)省了 0.9 秒!

內(nèi)聯(lián)函數(shù)、內(nèi)聯(lián)緩存和遞歸

一個(gè)名為 monkeys 的函數(shù)只因其名就引起了我的興趣。在調(diào)試中,我可以看到它在其內(nèi)部被多次調(diào)用,這是一個(gè)有力證據(jù),表明這里正在發(fā)生某種遞歸。它通常用于遍歷樹狀結(jié)構(gòu)。每當(dāng)使用某種遍歷時(shí),它就有可能在代碼的“熱路徑”中。并非所有情況都如此,但根據(jù)個(gè)人經(jīng)驗(yàn),這是一個(gè)很好的經(jīng)驗(yàn)法則。

function perItem(data, info, plugin, params, reverse) {
  function monkeys(items) {
    items.children = items.children.filter(function (item) {
      // 反向通過
      if (reverse && item.children) {
        monkeys(item)
      }
      // 主要過濾
      let kept = true
      if (plugin.active) {
        kept = plugin.fn(item, params, info) !== false
      }
      // 直接通過
      if (!reverse && item.children) {
        monkeys(item)
      }
      return kept
    })
    return items
  }
  return monkeys(data)
}

這里我們有一個(gè)函數(shù),它在其體內(nèi)創(chuàng)建另一個(gè)函數(shù),該函數(shù)再次調(diào)用內(nèi)部函數(shù)。如果我不得不盲猜一手,我會假設(shè)這樣做是為了節(jié)省一些敲鍵次數(shù),而不必再次傳遞所有參數(shù)。問題是,當(dāng)外部函數(shù)被頻繁調(diào)用時(shí),在其他函數(shù)內(nèi)部創(chuàng)建的函數(shù)很難優(yōu)化。

function perItem(items, info, plugin, params, reverse) {
  items.children = items.children.filter(function (item) {
    // 反向通過
    if (reverse && item.children) {
      perItem(item, info, plugin, params, reverse)
    }
    // 主要過濾
    let kept = true
    if (plugin.active) {
      kept = plugin.fn(item, params, info) !== false
    }
    // 直接通過
    if (!reverse && item.children) {
      perItem(item, info, plugin, params, reverse)
    }
    return kept
  })
  return items
}

我們可以通過始終顯式傳遞所有參數(shù),而不是像以前一樣通過閉包捕獲它們,從而擺脫內(nèi)部函數(shù)。此變更的影響相當(dāng)小,但總共又節(jié)省了 0.8 秒。

小心 for..of 的轉(zhuǎn)譯

@vanilla-extract/css 中出現(xiàn)了幾乎相同的問題。已發(fā)布的軟件包附帶以下代碼:

class ConditionalRuleset {
  getSortedRuleset() {
    //...
    var _loop = function _loop(query, dependents) {
      doSomething()
    }

    for (var [query, dependents] of this.precedenceLookup.entries()) {
      _loop(query, dependents)
    }
    //...
  }
}

這個(gè)函數(shù)的有趣之處在于,它沒有出現(xiàn)在原始源碼中。在原始源碼中,它是一個(gè)標(biāo)準(zhǔn)的 for...of 循環(huán)。

class ConditionalRuleset {
  getSortedRuleset() {
    //...
    for (var [query, dependents] of this.precedenceLookup.entries()) {
      doSomething()
    }
    //...
  }
}

我無法在 babel 或 typescript 的 repl 中復(fù)現(xiàn)此問題,但我可以確定它是由它們的構(gòu)建管道引入的。鑒于這似乎是構(gòu)建工具的共享抽象,我假設(shè)還有更多項(xiàng)目受到此影響。因此,現(xiàn)在我只是在 node_modules 內(nèi)部本地修補(bǔ)了該包,并且很高興看到,這縮短了 0.9s 的構(gòu)建時(shí)間。

semver 的奇怪案例

對于此例子,我不確定我是否配置錯(cuò)誤。本質(zhì)上,該配置文件表明,每當(dāng)轉(zhuǎn)譯文件時(shí),整個(gè) babel 配置總是會被重新讀取。

在屏幕截圖中很難看出,但占用大量時(shí)間的函數(shù)之一是 semver 包中的代碼,該包與 npm 的 cli 中使用的包相同。我花了一段時(shí)間才明白:它是用于解析 @babel/preset-env 的 browserlist 目標(biāo)。盡管瀏覽器列表設(shè)置可能看起來很短,但最終它們擴(kuò)展到大約 290 個(gè)單獨(dú)的目標(biāo)。

僅這一點(diǎn)還不足以引起關(guān)注,但在使用驗(yàn)證函數(shù)時(shí)很容易忽略分配成本。它在 babel 的代碼庫中有點(diǎn)分散,但本質(zhì)上瀏覽器目標(biāo)的版本被轉(zhuǎn)換為 semver 字符串 "10" -> "10.0.0",然后進(jìn)行驗(yàn)證。其中一些版本號已經(jīng)與 semver 格式匹配。這些版本(有時(shí)是版本范圍)會相互比較,直到找到需要轉(zhuǎn)譯的最低通用功能集。此方案問題不大。

這里出現(xiàn)了性能問題,因?yàn)?nbsp;semver 版本存儲為 string,而不是解析的 semver 數(shù)據(jù)類型。這意味著,每次調(diào)用 semver.valid('1.2.3') 都會創(chuàng)建一個(gè)新的 semver 實(shí)例,并立即銷毀它。使用字符串 semver.lt('1.2.3', '9.8.7') 比較 semver 版本時(shí)也是如此。這就是為什么我們在調(diào)試中看到如此明顯的 semver。

完美謝幕

我假設(shè)您會在流行庫中發(fā)現(xiàn)更多這些性能細(xì)節(jié)瓶頸。今天我們主要關(guān)注若干構(gòu)建工具,但 UI 組件或其他庫通常也有同樣容易出現(xiàn)的性能問題。

免責(zé)聲明

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

以上就是JS生態(tài)系統(tǒng)加速一次一庫PostCSS SVGO的重構(gòu)源碼和性能優(yōu)化探索的詳細(xì)內(nèi)容,更多關(guān)于JS PostCSS SVGO的資料請關(guān)注腳本之家其它相關(guān)文章!

相關(guān)文章

最新評論