成人免费xxxxx在线视频软件_久久精品久久久_亚洲国产精品久久久_天天色天天色_亚洲人成一区_欧美一级欧美三级在线观看

Webpack4的SourceMap階段的性能優(yōu)化和踩坑

開發(fā) 前端
SourceMap生成流程 SourceMap 生成過程中,由于項目過大導(dǎo)致需要計算處理的映射節(jié)點(SourceNode)特別多(遇到過10^6數(shù)量級的項目),這也導(dǎo)致 SourceMap 生成過程中內(nèi)存飆升頻繁 GC,構(gòu)建十分緩慢甚至 OOM。

Hello,大家好,我是松寶寫代碼,寫寶寫的不止是代碼。

由于優(yōu)化都是在 Webpack 4 上做的,當時 Webpack 5 還未穩(wěn)定,現(xiàn)在使用 Webpack 5 時可能有些優(yōu)化方案不再需要或方案不一致,這里主要分享思路,可供參考。

背景

在接觸一些大型項目構(gòu)建速度慢的很離譜,有些項目在 編譯構(gòu)建上30分鐘超時,有些構(gòu)建到一半內(nèi)存溢出。但當時一些通用的 Webpack 構(gòu)建優(yōu)化方案要么已經(jīng)接入,要么場景不適用:

  • 已接入的方案效果有限。比如 cache-loader、thread-loader,能優(yōu)化編譯階段的速度,但對于依賴解析、代碼壓縮、SourceMap 生成等環(huán)節(jié)無能為力
  • 作為前端基建方案,業(yè)務(wù)依賴差異極大,難以針對特定依賴優(yōu)化,如 DllPlugin 方案
  • 作為移動端打包方案,追求極致的首屏加載速度,難以接受頻繁的異步資源請求,如 Module Federation、Common Chunk 方案
  • 存在一碼多產(chǎn)物場景,需要單倉庫多模式構(gòu)建(1.0/2.0 * 主包/分包)下緩存復(fù)用,難以接受耦合度高的緩存方案,如 Persistent Caching

在這種情況下,只好另辟蹊徑去尋找更多優(yōu)化方案,這篇文章主要就是介紹這些“非主流”的優(yōu)化方案,以及引發(fā)的思考。

今天帶來的是webapck4sourceMap階段。

SourceMap階段

SourceMap生成流程 SourceMap 生成過程中,由于項目過大導(dǎo)致需要計算處理的映射節(jié)點(SourceNode)特別多(遇到過10^6數(shù)量級的項目),這也導(dǎo)致 SourceMap 生成過程中內(nèi)存飆升頻繁 GC,構(gòu)建十分緩慢甚至 OOM。

Webpack 內(nèi)部有大量的代碼拼接工作,而每一次代碼拼接都涉及到 SourceMap 的處理,因此 Webpack 內(nèi)封裝了 webpack-sources,其中 SourceMapSource 用于保存 SourceMap,ConcatSource 用于代碼拼接, SourceMap 操作使用 source-map 和 source-list-map 庫來處理。

而其內(nèi)部實際上是在運行 sourceAndMap()/map() 方法時才進行計算:

// webpack-sources/SourceMapSource
class SourceMapSource extends Source {
  // ...
  node(options) {
    // 此處進行真正的計算
    var sourceMap = this._sourceMap;
    var node = SourceNode.fromStringWithSourceMap(this._value, new SourceMapConsumer(sourceMap));
    node.setSourceContent(this._name, this._originalSource);
    var innerSourceMap = this._innerSourceMap;
    if(innerSourceMap) {
      node = applySourceMap(node, new SourceMapConsumer(innerSourceMap), this._name, this._removeOriginalSource);
    }
    return node;
  }
  // ...
}
// webpack-sources/SourceAndMapMixin
proto.sourceAndMap = function (options) {
  options = options || {};
  if (options.columns === false) {
    return this.listMap(options).toStringWithSourceMap({
      file: "x",
    });
  }

  var res = this.node(options).toStringWithSourceMap({
    file: "x",
  });
  return {
    source: res.code,
    map: res.map.toJSON(),
  };
};

SourceMap 優(yōu)化方案

很顯然,如果把所有模塊的 SourceMap 都放到最后一起來計算,對主進程長時間占用導(dǎo)致 SourceMap 生成緩慢。可以通過如下方法進行優(yōu)化:

  1. 使用行映射 SourceMap
  2. SourceMap 并行化
  3. SourceNode 內(nèi)存優(yōu)化

行映射 SourceMap

SourceMap 的本質(zhì)就是大量的 SourceNode 組成,每一個 SourceNode 存儲了產(chǎn)物的位置到源碼位置的映射關(guān)系。通常映射關(guān)系是行號+列號,但我們排查bug時候一般只看哪一行,具體哪一列看的不多。如果忽略列號則可以大幅度減少 SourceNode 的數(shù)量。

SourceMapDevToolPlugin 中的 columns 設(shè)為 true 時就是行映射 SourceMap。但這個插件處理的邏輯已經(jīng)是在最后產(chǎn)物生成階段,而在整個 Webpack 構(gòu)建流程中流轉(zhuǎn)的 SourceMap 依然是行列映射。因此可以直接代理掉 SourceMapSource 的 map 方法,寫死 columns 為 true。

SourceMap 并行化

SourceMap 最后一起堆積在主進程中生成是非常緩慢的,因此可以考慮在模塊級壓縮的時候,手動模擬 node() 方法,觸發(fā)一下 applySourceMap 方法提前生成 SourceNode,并將 SourceNode 序列化傳遞回主進程,當主進程需要使用時直接獲取即可。

SourceNode 內(nèi)存優(yōu)化

當字符串被 split 時,行為與 substr 不太一樣,split 會生成字符串的拷貝,占用額外的內(nèi)存(chrome memory profile 中為string),而 substr 會生成引用,字符串不會拷貝占用額外內(nèi)存(chrome memory profile 中為 sliced string),但與此同時也意味著父字符串無法被 GC 回收。

const bigstring = '00000\n'.repeat(50000);
console.log(bigstring); // 觸發(fā)生成
const array = bigstring.split('\n');

圖片

const bigstring = '00000\n'.repeat(500000);
console.log(bigstring)
const array = [];
for (let i = 0; i < 100000;i++) {
  array.push(bigstring.substr(i*5,i*5+5));
}

圖片


而看 source-map 中 SourceNode 的代碼可以發(fā)現(xiàn):

  • SourceNode 會將完整代碼根據(jù)換行符 split 切分(生成大量 string 內(nèi)存占用)。
  • 根據(jù) mapping 對代碼求子串并保存(此時意味著這些 string 無法被釋放)。
// source-map/SourceNode
SourceNode.fromStringWithSourceMap = function SourceNode_fromStringWithSourceMap(
  aGeneratedCode,
  aSourceMapConsumer,
  aRelativePath
) {
  // ...
  // 此處進行了代碼切分
  var remainingLines = aGeneratedCode.split(REGEX_NEWLINE);
  // ...

  aSourceMapConsumer.eachMapping(function (mapping) {
    if (lastMapping !== null) {
      if (lastGeneratedLine < mapping.generatedLine) {
        // ...
      } else {
        var nextLine = remainingLines[remainingLinesIndex] || '';
        // 此處獲取子串并長久保存
        var code = nextLine.substr(0, mapping.generatedColumn - lastGeneratedColumn);
        // ...
        addMappingWithCode(lastMapping, code);
        // No more remaining code, continue
        lastMapping = mapping;
        return;
      }
    }
    //...
  }, this);
  // ...
};

那么這個昂貴的 "code" 字段干什么用的呢?實際上只有如下兩個功能:

  1. 每一個 code 都會生成一個子 SourceNode,而最終遞歸生成的子 SourceNode 在 walk 階段又會拼接回產(chǎn)物代碼。
  2. 如果包含了換行符,則會用來做映射位置的偏移計算。
// source-map/SourceNode
SourceNode.prototype.toStringWithSourceMap = function SourceNode_toStringWithSourceMap(aArgs) {
  // ...
  this.walk(function (chunk, original) {
    generated.code += chunk;
    //...
    for (var idx = 0, length = chunk.length; idx < length; idx++) {
      if (chunk.charCodeAt(idx) === NEWLINE_CODE) {
        generated.line++;
        generated.column = 0;
        // Mappings end at eol
        // ...
      } else {
        generated.column++;
      }
    }
  });
  this.walkSourceContents(function (sourceFile, sourceContent) {
    map.setSourceContent(sourceFile, sourceContent);
  });

  return { code: generated.code, map: map };
};

那么問題來了,產(chǎn)物代碼有很多其他渠道能夠獲取不需要在這里計算。而僅僅為了換行計算浪費如此大量的內(nèi)存顯然是不合理的。因此可以在一開始就把換行符的位置計算出來,保留在 SourceNode 內(nèi)部,然后讓切分出來的字符被 GC 回收,等到 walk 的時候直接拿這些換行符記錄進行計算即可。

衍生的應(yīng)用場景

思路

前面構(gòu)建生成了緩存,我們希望緩存是可移植、可拼接、預(yù)生成的:

  1. 可移植:中間產(chǎn)物不依賴特定環(huán)境,放到其他場景下依然能夠使用。
  2. 可拼接:對于每一個項目都有自己的中間產(chǎn)物,而當一個聚合的項目使用這些項目時,也可以通過聚合生成自己的中間產(chǎn)物。
  3. 預(yù)生成:中間產(chǎn)物可以提前生成,存放到云端,在任何有需要的場景下載使用。

通過預(yù)生成,按需下發(fā),動態(tài)拼接的方式,就能真正做到“絕不構(gòu)建第二次”。

可移植緩存

緩存與環(huán)境解耦是可以讓緩存跨機器使用,遺憾的是 Webpack 在其模塊的 request 中包含絕對路徑(要找到對應(yīng)的文件),導(dǎo)致與其相關(guān)的 AST 解析、模塊 ID 生成等等都受到影響。因此要做到可移植緩存,需要如下改造:

  1. 統(tǒng)一的緩存管理:不受控的緩存難以做后續(xù)的環(huán)境解耦。
  2. 路徑替換&復(fù)原:對于寫入緩存的所有內(nèi)容,一旦出現(xiàn)了本地路徑,都需要替換成占位符。讀取時則需要將占位符恢復(fù)成新環(huán)境的路徑。
  3. AST 偏移矯正:由于路徑替換過程中,路徑長度發(fā)生變化,從而導(dǎo)致上述依賴解析階段的 AST 位置信息緩存失效,因此需要根據(jù)路徑長度差異對 AST 位置進行矯正。
  4. Hash 代理:由于構(gòu)建流程中有大量的 Hash 生成場景,而一旦包含了本地路徑字符串加入到 Hash 生成中,則必然導(dǎo)致 Hash 在新環(huán)境下無法被匹配。

圖片

增量的構(gòu)建

有了可移植的緩存,就能實現(xiàn)增量的構(gòu)建。核心思路如下:

  • 項目某個特定版本源碼作為項目基線,基線初始化構(gòu)建生成基線緩存和基線文件元數(shù)據(jù)
  • 當文件發(fā)生變化時:
  • 收集變化的文件生成變更元數(shù)據(jù)。
  • 變更元數(shù)據(jù) + 基線緩存 + 基線文件元數(shù)據(jù),構(gòu)建生成變更后產(chǎn)物+熱更新產(chǎn)物,同時產(chǎn)出增量補丁。
  • 增量補丁主要包含文件目錄的增量、緩存的增量。
  • 如果有前代增量補丁,可以合并。
  • 當環(huán)境發(fā)生變化時,在新環(huán)境下:
  • 增量補丁+基線緩存+基線文件元數(shù)據(jù),通過增量消費構(gòu)建,也可以再次產(chǎn)出構(gòu)建產(chǎn)物。
  • 當需要提升一個特定增量補丁的版本作為基線時,將其增量變更與基線緩存、基線文件元數(shù)據(jù)合并即可。

圖片

增量構(gòu)建最大的好處:解決長迭代鏈導(dǎo)致的緩存存儲成本爆炸問題。

舉個例子:如果要做一個類似于 codepen、jsfiddle 那樣的 playground,可以在線編輯項目代碼,迭代中的每次編輯都可以回退,同時也能隨時將一次修改派生成為一個新的迭代。

在這種場景下,顯然不能給每次代碼修改都完整復(fù)刻一套緩存。增量的構(gòu)建僅需要保存一個基線和對應(yīng)版本相對于基線的增量,當切換到一個特定版本時,使用基線+增量就可以編譯出最新的產(chǎn)物,實現(xiàn)版本的快速恢復(fù)。這個同理可以應(yīng)用在項目自身迭代過程的構(gòu)建緩存池中。

最后

一些思考

  • 函數(shù)編寫:牢記“引用透明”原則,這是緩存、并行化的基本前提。
  • 模型設(shè)計:保證可序列化/反序列化,為緩存、并行化打好基礎(chǔ)。
  • 緩存設(shè)計:所有緩存應(yīng)當結(jié)構(gòu)簡單且路徑無關(guān),保證緩存可移植、可拼接。
  • 對象引用:盡早釋放巨大對象的引用,僅保留需要的數(shù)據(jù)。
  • 插件機制:tapable 這種 pub/sub 機制是否真的合理且靈活,也許高階函數(shù)更加合適。
  • TypeScript:非 TS 代碼閱讀難度很大,運行時的數(shù)據(jù)流不去 debug 無法理解。

一些腦洞:

圖片

責(zé)任編輯:姜華
相關(guān)推薦

2023-04-27 08:35:20

Webpack 4性能優(yōu)化

2020-09-19 21:26:56

webpack

2021-06-09 08:21:14

Webpack環(huán)境變量前端

2022-08-26 13:24:03

version源碼sources

2019-03-15 15:00:49

Webpack構(gòu)建速度前端

2010-05-05 11:48:27

Oracle設(shè)計開發(fā)階

2021-11-09 09:57:46

Webpack 前端分包優(yōu)化

2023-02-20 08:11:04

2021-12-15 09:21:59

Webpack 前端Sourcemap

2019-03-05 10:20:49

WebWebpack分離數(shù)據(jù)

2021-12-28 08:17:41

循環(huán) forgo

2017-07-17 15:46:20

Oracle并行機制

2024-04-10 08:39:56

BigDecimal浮點數(shù)二進制

2024-04-01 08:05:27

Go開發(fā)Java

2010-04-21 14:00:48

Oracle數(shù)據(jù)庫

2022-09-23 15:23:08

webpack5改變代碼

2016-09-21 13:17:31

LibreOfficeJava緩沖區(qū)

2019-03-26 10:02:16

WebpackJavascript前端

2024-11-26 08:20:53

程序數(shù)據(jù)歸檔庫

2018-01-10 13:40:03

數(shù)據(jù)庫MySQL表設(shè)計
點贊
收藏

51CTO技術(shù)棧公眾號

主站蜘蛛池模板: 亚洲欧美在线一区 | 亚洲国产精品美女 | 一区二区三区在线播放 | 在线国产一区二区 | 国产福利二区 | 91国在线观看 | 在线观看成人免费视频 | 男人视频网站 | 成年网站在线观看 | 国产精品一区二区三区久久久 | 国产欧美一区二区在线观看 | 久久国产一区二区三区 | 欧美在线观看一区二区 | 亚洲精品视频免费 | 一区精品视频在线观看 | 91网站在线播放 | 91在线中文字幕 | 成人网址在线观看 | www.久久| 成人h视频在线 | 欧美日韩国产一区二区 | 高清一区二区三区 | 久久久久久免费毛片精品 | 99在线观看视频 | 欧美中文字幕 | 国产一区二区三区在线 | 成年人视频免费在线观看 | 国产精品一区二区久久精品爱微奶 | 国产亚洲精品美女久久久久久久久久 | 国产亚洲精品久久久久动 | 亚洲色图综合网 | 国产精品久久久久久久久久 | 欧美福利在线 | 一级黄色录像毛片 | 91一区二区在线观看 | 天天操夜夜操免费视频 | 最大av在线 | 欧美日韩a | 中文字幕av网 | 欧美天堂在线 | 欧美老少妇一级特黄一片 |