淺析 Parcel 的 Rust 打包算法 Demo
Parcel 是一個類似于 Webpack 、Rollup 的構(gòu)建工具,相較于這一類構(gòu)建工具,Parcel 主打的賣點(diǎn)是零配置并開箱即用,雖然某種程度上這種零配置的方式會使得項(xiàng)目定制化變得很困難,但 Parcel 盡量提供了一套自身的構(gòu)建最佳實(shí)踐,以后有機(jī)會去單獨(dú)介紹一下 Parcel 的整體構(gòu)造,這里不展開講解了。
Parcel 在 2.8.0 的更新中提到使用了一個新的打包算法,相比較于之前速度提升了 2.7 倍,并且體積還減小了 2.5 倍。同時還有其他的比較夸張的性能提升,例如 6 倍的熱更新速度,增量構(gòu)建的再次構(gòu)建性能提升了10倍。
同時作者強(qiáng)調(diào)該算法是由來自 Atlassian 的團(tuán)隊(duì)貢獻(xiàn)的,他們?yōu)榇嘶舜蠹s一年的時間使得其在 parcel v2.8.0 中成為默認(rèn)的打包算法,該算法帶來了巨大的性能提升,并且通過更少的重復(fù)包以及更好的瀏覽器緩存來有效減少了包產(chǎn)物的體積:
This results in both smaller bundles and much faster builds. *For a very large real-world project with over 60,000 assets, overall build time was reduced from over 25 minutes to 9 minutes (2.7x faster). The total bundle size for the whole project went from 952 MB *to 370 MB (2.5x smaller). For comparison, building the same app with webpack takes over 45 minutes.
此處測試項(xiàng)目根據(jù) twitter 信息為 jira。
實(shí)際上這個算法在 landing 過程中,它是基于 Parcel 作者 Devon Govett 本身寫的一個打包算法原型,并對這個算法本身在 Parcel 的實(shí)際場景中做了一些優(yōu)化。具體可以參考這個 PR: https://github.com/parcel-bundler/parcel/pull/6975 的一些內(nèi)容:
在這篇文章中,我們先暫時不分析 Parcel 目前的具體的打包策略以及代碼邏輯,而是結(jié)合這個原型倉庫來了解一下 Parcel 最原始的打包算法的運(yùn)行思路:
圖片
相比較于 Parcel 本身的打包算法,這個原型 Demo 可以要更簡單一點(diǎn)(不過由于代碼是 Rust,理解難度對筆者來說也沒有很簡單),在之后的文章中,我會單獨(dú)結(jié)合 Parcel 目前本身的打包算法(Default Bundlers)來做一下講解。
Rust 算法 demo
我們可以根據(jù)上面 PR 中找到對應(yīng)的算法原型倉庫,這個倉庫的地址是: https://github.com/devongovett/bundler-algorithm, 不過由于倉庫的一些內(nèi)容已經(jīng)實(shí)際落地了,因此作者后面將這個倉庫給歸檔了。
這個倉庫是基于 rust 寫的,不過整體流程上而言并不是特別復(fù)雜,我們可以簡單調(diào)試一下(本地需要有 rust 環(huán)境):
git clone https://github.com/devongovett/bundler-algorithm
cd bundler-algorithm
# 使用 nightly 版本的 cargo
rustup install nightly && rustup default nightly
# 運(yùn)行 demo
cargo run
根據(jù)該倉庫源碼,我們可以將一次打包算法執(zhí)行流程分為下面幾個步驟,注意以下步驟中的具體代碼存在一些細(xì)節(jié)省略的情況,如果你想了解更多的算法細(xì)節(jié),可以參考倉庫源碼來進(jìn)行閱讀。
構(gòu)建依賴圖
首先在 main.rs 文件中的第一步,會先根據(jù)項(xiàng)目各文件之間的引用關(guān)系構(gòu)建一個依賴圖出來。
這里直接參考 build_graph 這個方法,例如這里項(xiàng)目中存在 html 、js 等不同類型的文件,首先會將這些資源文件作為節(jié)點(diǎn)添加到一個圖中,然后根據(jù)這些文件之間的引用關(guān)系去給對應(yīng)的節(jié)點(diǎn)添加對應(yīng)的邊,這樣就會形成一個比較初步完善的依賴圖。
// 創(chuàng)建 graph 并獲取對應(yīng)的 entries 對象
let (g, entries) = build_graph();
fn build_graph<'a>() -> (Graph<Asset<'a>, Dependency>, Vec<NodeIndex>) {
let mut g = Graph::new();
let mut entries = Vec::new();
let html = g.add_node(Asset {
name: "a.html",
asset_type: AssetType::HTML,
size: 10
});
let js = g.add_node(Asset {
name: "a.js",
asset_type: AssetType::JavaScript,
size: 10
});
// ...一些資源的初始化過程,這里節(jié)省篇幅跳過...
g.add_edge(html, js, Dependency {
is_async: false
});
entries.push(html);
entries.push(html2);
return (g, entries);
}
由于 build_graph 方法中代碼邏輯都比較單一,因此上面貼的源碼中有一些重復(fù)邏輯例如資源的初始化等的省略。
最后這個方法會構(gòu)建出一個如下圖所示的依賴圖出來,注意下面還有一些依賴之間的引用關(guān)系是異步的(這里可以理解為動態(tài)導(dǎo)入),同時這里我們給每個靜態(tài)資源都標(biāo)注一個唯一的 asset_id,下文圖中標(biāo)注的序號均與此處對應(yīng):
圖片
其中 Parcel 會默認(rèn)將 Html 資源作為依賴圖的入口文件,因此我們可以看到這張圖是存在兩個入口的,分別為 a.html 和 b.html 。
遍歷圖創(chuàng)建獨(dú)立的 bundle
這里的 bundle 可以簡單理解為最終 打包 輸出的文件,這個可以結(jié)合 Webpack 里面的一些概念來理解。
在圖創(chuàng)建完成之后,這一步主要目的則是根據(jù) graph 提供的一些信息(例如 entry、資源之間的依賴關(guān)系等)去創(chuàng)建出最后打包出來的 bundle 類型,這里首先會對下面三種情況創(chuàng)建一個單獨(dú)的 bundle:
- 入口文件,例如這里的 html 入口文件
- 不同類型資源之間的引用,例如當(dāng) js 文件中引用到圖片資源的時候
- 資源之間存在 異步 引用時,例如某個 js 動態(tài)導(dǎo)入另外的 js 文件
下面我將根據(jù)這幾種情況來展開講講:
首先是根據(jù)入口文件去創(chuàng)建 bundle,這里邏輯很簡單,遍歷一下前面 build_graph 方法生成的 entries 數(shù)組(實(shí)際上這里是個 Rust 的 Vec ),然后往 bundle_roots 中插入對應(yīng)的 entry 以及 bundle_id 。
這里的 bundle_id 對應(yīng)前面 graph 圖中標(biāo)記的序列號,例如 a.html 是 0,b.html 是 1。具體的實(shí)現(xiàn)參考以下邏輯:
let mut bundle_roots = HashMap:new();
let mut reacheable_bundles = HashSet::new();
let mut bundle_graph = Graph::new();
// 遍歷 entries,往 bundle_roots 中插入對應(yīng)的 entry 信息以及對應(yīng)的 bundle_id
for entry in &entries {
let bundle_id = bundle_graph.add_node(Bundle::from_asset(*entry, &g[*entry]));
bundle_roots.insert(*entry, (bundle_id, bundle_id));
}
這里應(yīng)用到實(shí)際開發(fā)中的場景可以聯(lián)想到我們開發(fā)一個單頁(SPA) 或者多頁應(yīng)用(MPA)時,在打包產(chǎn)物中通常會出現(xiàn)一個或者多個 html 入口文件,這里的情況也是類似的。
添加完 html 入口文件之后,接下來就會用深度優(yōu)先搜索算法(DFS)去遍歷整個圖,對以下兩種情況再生成單獨(dú)的 bundle:
- 不同類型資源之間的引用,對被引用的資源創(chuàng)建一個 bundle,例如 a.js 中引用到了 style.css 那么 style.css 會被處理成一個單獨(dú)的 bundle
- 資源之間存在 異步 引用時,對被引用的資源創(chuàng)建一個 bundle,例如 a.js 是異步引用的 async.js ,那么 async.js 會被處理成一個單獨(dú)的 bundle
以下為遍歷圖的主要代碼邏輯,具體可以參考 DfsEvent::TreeEdge(u, v) ,這里是遍歷圖中的各個相連子節(jié)點(diǎn):
let mut stack = LinkedList::new();
depth_first_search(&g, entries, |event| {
match event {
// ...
DfsEvent::TreeEdge(u, v) => {
let asset_a = &g[u];
let asset_b = &g[v];
// 當(dāng)資源類型發(fā)生變化的時候,創(chuàng)建一個新的 bundle
if asset_a.asset_type != asset_b.asset_type {
// 舉個例子,這里 a.js -> style.css
// 這里 bundle_group_id 是 a.html,asset_b 是 style.css
// style.css 是不同類型的資源被引用,會被拆單獨(dú)的 bundle 出來
let (_, bundle_group_id) = stack.front().unwrap();
let bundle_id = bundle_graph.add_node(Bundle::from_asset(v, asset_b));
bundle_roots.insert(v, (bundle_id, *bundle_group_id));
bundle_graph.add_edge(*bundle_group_id, bundle_id, 0);
return
}
// 當(dāng)存在異步依賴的時候,創(chuàng)建一個新的 bundle
let dependency = &g[g.find_edge(u, v).unwrap()];
// 舉個例子,這里 a.js -> async.js 是異步依賴導(dǎo)入
if dependency.is_async {
// 因此這里 async.js(這里的 asset_b) 會被處理成一個單獨(dú)的 bundle
let bundle_id = bundle_graph.add_node(Bundle::from_asset(v, asset_b));
bundle_roots.insert(v, (bundle_id, bundle_id));
for (b, _) in &stack {
let a = &g[*b];
if a.asset_type != asset_b.asset_type {
break
}
reachable_bundles.insert((*b, v));
}
}
}
// ...
}
});
在經(jīng)歷過上面兩次操作之后,我們最開始的 Graph 就會創(chuàng)建以下幾個單獨(dú)的 bundle 出來,對應(yīng)的文件分別為:
- 作為入口文件的 a.html 和 b.html 各自形成一個 bundle
- 被 html 文件引用的 a.js 和 b.js 以及被 js 文件引用的 style.css 各自形成一個 bundle
- 被異步引用的 async.js 文件形成一個 bundle
并且這些 bundle 之間還存在一定的依賴關(guān)系(這里的序號為最初始依賴圖中的序號):
圖片
上面 async2.js 這個異步的 js 資源沒有形成單獨(dú)的 bundle 原因在于其是被其它 js 資源同步引入的。
處理所有資源到對應(yīng) bundle
在上一接中利用初始化的依賴圖分析出了最初的幾個獨(dú)立的 bundle,當(dāng)然還剩下一些其它的資源還沒處理,例如 async2.js 和 shared.js 這兩個 asset。
因此這一步的操作就是將這些還沒有處理成 bundle 的 assets 全部都處理到 bundles 中去,同時整合一下上一步遍歷出來的獨(dú)立 bundle。
首先這一步會根據(jù)前面的到的獨(dú)立 bundle 去建立一個可訪問的節(jié)點(diǎn),這里會用輪詢上一節(jié)生成的獨(dú)立 bundle,也就是這里的 bundle_roots ,同時根據(jù)每個 bundle_root 在最原始的依賴圖中進(jìn)行一次 DFS 查找,然后剪枝掉一些同樣是 bundle_root 的 bundle,把剩余不是 asset 的添加到可訪問節(jié)點(diǎn)中來,實(shí)現(xiàn)參考如下:
let mut reachable_nodes = HashSet::new();
for (root, _) in &bundle_roots {
depth_first_search(&g, Some(*root), |event| {
if let DfsEvent::Discover(n, _) = &event {
if n == root {
return Control::Continue
}
// 如果命中了 bundle_root 就跳過
if bundle_roots.contains_key(&n) {
return Control::<()>::Prune;
}
// 否則就添加到可訪問節(jié)點(diǎn)中來
reachable_nodes.insert((*root, *n));
}
Control::Continue
});
}
舉個例子來說,例如 a.js 是個單獨(dú)的 bundle,但他同步引用了一個同類型的 async2.js ,同時 async2.js 又引用了一個 shared.js 。
圖片
那么這里就會形成一個以a.js為根節(jié)點(diǎn),分別到 async2.js 和 shared.js 的兩個可訪問節(jié)點(diǎn)。
同理,在上圖中我們還能找到這樣的一些訪問節(jié)點(diǎn):
圖片
拿到這些可訪問節(jié)點(diǎn)以后,這里會像最開始的的時候,根據(jù)這些節(jié)點(diǎn)再去形成一個可訪問節(jié)點(diǎn)的依賴圖,這個圖對比最初的依賴圖會小一點(diǎn),這個依賴圖的主要作用是為了幫助后續(xù)決定具體哪些 asset 被放到哪個 bundle 中去,這個過程可以和 Webpack 的拆包策略一起理解。
// 根據(jù)拿到的可訪問的節(jié)點(diǎn),構(gòu)造一個 graph 出來
let reachable_graph = Graph::<(), ()>::from_edges(&reachable_nodes);
這里的依賴圖實(shí)際上就是個去除一些 root_bundle 后的縮減版的依賴圖:
圖片
在完成可訪問節(jié)點(diǎn)依賴圖的構(gòu)建之后,下一步開始利用這個依賴圖將所有的 asset 都處理進(jìn) bundle 中去。其中這里每個 asset 都會基于其和可訪問的 bundle_roots 之間的關(guān)系被放到單獨(dú) bundle 中去,這么做的目的是為先創(chuàng)建盡可能沒有重復(fù)代碼的 bundle graph。
// 用于存儲 entry asset id 到 bundle id 的映射
let mut bundles: HashMap<Vec<NodeIndex>, NodeIndex> = HashMap::new();
for asset_id in g.node_indices() {
let reachable: Vec<NodeIndex> = reachable_graph.neighbors_directed(asset_id, Incoming).collect();
let reachable: Vec<NodeIndex> = reachable.iter().cloned().filter(|b| {
(&reachable).into_iter().all(|a| !reachable_bundles.contains(&(*a, *b)))
}).collect();
// 根據(jù)上面的每個 asset 對應(yīng)的可訪問的 bundle 去生成一個 bundle_graph
if let Some((bundle_id, _)) = bundle_roots.get(&asset_id) {
// 如果 asset 是個 bundle_root,那么會給這個 bundle_root 在 bundle_graph 添加上對應(yīng)的節(jié)點(diǎn)
bundles.entry(vec![asset_id]).or_insert(*bundle_id);
for a in &reachable {
if *a != asset_id {
bundle_graph.add_edge(bundle_roots[a].1, *bundle_id, 0);
}
}
} else if reachable.len() > 0 {
// 如果 asset 對于多個 bundle 都是可訪問的
// 例如 shared.js(6) 就同時被 a.js(2) 和 b.js(5) 可訪問
// 這里可以根據(jù)這些 asset 組合為該 asset 創(chuàng)建一個新的 bundle
}
}
這一步的代碼邏輯會很復(fù)雜(從 rust 代碼的角度來看,畢竟筆者不是很懂 rust QAQ),因此這里筆者并沒有貼完所有的代碼。但我這里會結(jié)合具體的代碼來舉例講解一下這一步的具體操作:
首先這一步會遍歷所有的 asset ,找到那些目前還沒有形成 bundle 的 asset,如下圖所示,經(jīng)歷過前一個步驟之后,沒有形成 root bundle 的 asset 的只有 async2.js 以及 shared.js 。
圖片
這里針對 async2.js 這個 asset 來講解一下,對于 async2.js 來說,對它可訪問的 bundle 有 a.js (2)、async.js (3)。這里由于 async2.js 對于 async.js 的父 bundle a.js 同樣是可訪問的,這里在處理可訪問節(jié)點(diǎn)的時候會把 async.js 這個 asset 給移除掉,相當(dāng)于 async2.js 實(shí)際可訪問的 bundle 只有 a.js ,這里這樣處理的作用是為了后續(xù) asset 的 bundle 合并操作,可以參考如下代碼:
for asset_id in g.node_indices() {
let reachable: Vec<NodeIndex> = reachable_graph.neighbors_directed(asset_id, Incoming).collect();
// 這一步篩掉 async2.js
let reachable: Vec<NodeIndex> = reachable.iter().cloned().filter(|b| {
(&reachable).into_iter().all(|a| !reachable_bundles.contains(&(*a, *b)))
}).collect();
}
在這一步篩出對應(yīng)可訪問的 bundle 之后(reachable),接下來就開始去繼續(xù)構(gòu)造 bundle_graph ,這上一步中 bundle_graph 中先暫時放置了一些 root_bundle 以及他們之間的引用關(guān)系。在這一步,對于沒有形成 bundle 的 asset 進(jìn)行繼續(xù)的完善。
這一步代碼比較復(fù)雜,筆者并沒有完全貼出來,大概處理過程分成了兩個分支:
- 處理成 root_bundle 的 bundle,需要將他們有依賴關(guān)系的 bundle_graph 添加上對應(yīng)的邊
- 處理 root_bundle 之外的 asset 為 bundle(async2.js 和 shared.js)
- 如果這個 asset 被多個 root_bundle 依賴(可訪問),那么會給這個 asset 創(chuàng)建一個單獨(dú)的 bundle 并給它在 bundle_graph 加上對應(yīng)的邊,例如這里的 shared.js
- 如果這個 asset 只被一個 root_bundle 依賴(可訪問),那么會直接把這個 asset 添加到 root_bundle 的 bundle 中組成一個新的 bundle,例如這里的 async2.js
for asset_id in g.node_indices() {
// ... 省略掉獲取 reacheable 這個變量過程
if let Some((bundle_id, _)) = bundle_roots.get(&asset_id) {
// 1. 處理 root_bundle,這一步可以理解為給 bundle_graph 添加邊
bundles.entry(vec![asset_id]).or_insert(*bundle_id);
for a in &reachable {
if *a != asset_id {
bundle_graph.add_edge(bundle_roots[a].1, *bundle_id, 0);
}
}
} else if reachable.len() > 0 {
// 2. 處理 root_bundle 之外的 asset 為 bundle
let source_bundles = reachable.iter().map(|a| bundles[&vec![*a]]).collect();
let bundle_id = bundles.entry(reachable.clone()).or_insert_with(|| {
let mut bundle = Bundle::default();
bundle.source_bundles = source_bundles;
bundle_graph.add_node(bundle)
});
let bundle = &mut bundle_graph[*bundle_id];
bundle.asset_ids.push(asset_id);
bundle.size += g[asset_id].size;
// 處理完 asset 為 bundle 之后,同樣要添加邊
for a in reachable {
if a != *bundle_id {
bundle_graph.add_edge(bundle_roots[&a].1, *bundle_id, 0);
}
}
}
}
}
這一步最后會形成一個這樣的 bundle_graph ,這里對每個 bundle 進(jìn)行了重新標(biāo)號,不同于之前的 asset_id
圖片
這里我們可以看到, 其中 async2.js 和 root_bundle a.js 形成了一個新的 bundle,而另外一個 asset shared.js 也形成了一個單獨(dú)的 bundle,它們之間的依賴關(guān)系(graph 的邊)也能比較清晰的看到。
合并小的公共 bundle
上一步我們基本上將所有的 asset 都全處理成了 bundle 并成功構(gòu)建出了一個 bundle_graph ,實(shí)際上現(xiàn)在構(gòu)建大頭基本上已經(jīng)完成了。
下面的步驟基本就是對 bundle_graph 做一個優(yōu)化,這一步的處理對比前面的步驟就很簡單了。
在開始介紹代碼之前,這一步我們可以理解為 webpack 的 chunkSplit 配置中的 splitChunks.minSize 。在 Parcel 也有對應(yīng)的配置可以參考,這里的 minBundleSize:
{
"@parcel/bundler-default": {
"minBundles": 1,
"minBundleSize": 3000,
"maxParallelRequests": 20
}
}
大致就是對小于 minBundleSize 的 shared_bundle 合并到對應(yīng)的可訪問 bundle 中去,這里的副作用是可能會導(dǎo)致在多個 bundle 中存在一定體積的重復(fù) asset 體積(重復(fù)代碼),但帶來的好處則是可以減少請求數(shù)量,這里具體的取舍開始看用戶自身的配置。
這一步的代碼處理如下:
for bundle_id in bundle_graph.node_indices() {
let bundle = &bundle_graph[bundle_id];
// 當(dāng)這個 bundle 本身為 commen bundle 且他本身的體積小于某個值,這里的 demo 默認(rèn)寫為了 10
// 那么則可以合并掉這個 bundle
if bundle.source_bundles.len() > 0 && bundle.size < 10 {
remove_bundle(&g, &mut bundle_graph, bundle_id);
}
}
fn remove_bundle(
asset_graph: &Graph<Asset, Dependency>,
bundle_graph: &mut Graph<Bundle, i32>,
bundle_id: NodeIndex
) {
// 在 bundle_graph 中刪除掉對應(yīng)的 bundle
let bundle = bundle_graph.remove_node(bundle_id).unwrap();
// 并將該 bundle 合并對其有引用關(guān)系的 bundle 中去
for asset_id in &bundle.asset_ids {
for source_bundle_id in &bundle.source_bundles {
let bundle = &mut bundle_graph[*source_bundle_id];
bundle.asset_ids.push(*asset_id);
bundle.size += asset_graph[*asset_id].size;
}
}
合并超出并行請求限制的公共 bundle
在上一步處理完了最小體積的公共 bundle,這一步要處理的則是最大并行請求的 chunk,舉個例子,例如這里拆出來了 a.js 、common-a.js 、common-b.js 這三個 bundle,并且 a.js 同時依賴 common-a.js 和 common-b.js,但現(xiàn)在用戶配置允許的最大 bundle 并行請求數(shù)目是 2,那么這里就會合并掉這兩個 common bundle 中的其中一個到 a.js 中去。
這里在 Parcel 中也有對應(yīng)的配置項(xiàng),參考下面的 maxParallelRequests 這個參數(shù):
{
"@parcel/bundler-default": {
"minBundles": 1,
"minBundleSize": 3000,
"maxParallelRequests": 20
}
}
這一步的代碼處理可以參考如下:
// demo 這里默認(rèn)的限制請求數(shù)量為 3
let limit = 3;
for (_, (bundle_id, bundle_group_id)) in bundle_roots {
if bundle_id != bundle_group_id {
continue;
}
let mut neighbors: Vec<NodeIndex> = bundle_graph.neighbors(bundle_group_id).collect();
if neighbors.len() > limit {
neighbors.sort_by(|a, b| bundle_graph[*a].size.cmp(&bundle_graph[*b].size));
// Remove bundles until the bundle group is within the parallel request limit.
for bundle_id in &neighbors[0..neighbors.len() - limit] {
// Add all assets in the shared bundle into the source bundles that are within this bundle group.
let source_bundles: Vec<NodeIndex> = bundle_graph[*bundle_id].source_bundles.drain_filter(|s| neighbors.contains(s)).collect();
for source in source_bundles {
for asset_id in bundle_graph[*bundle_id].asset_ids.clone() {
let bundle_id = bundles[&vec![source]];
let bundle = &mut bundle_graph[bundle_id];
bundle.asset_ids.push(asset_id);
bundle.size += g[asset_id].size;
}
}
// Remove the edge from this bundle group to the shared bundle.
bundle_graph.remove_edge(bundle_graph.find_edge(bundle_group_id, *bundle_id).unwrap());
// If there is now only a single bundle group that contains this bundle,
// merge it into the remaining source bundles. If it is orphaned entirely, remove it.
let count = bundle_graph.neighbors_directed(*bundle_id, Incoming).count();
if count == 1 {
remove_bundle(&g, &mut bundle_graph, *bundle_id);
} else if count == 0 {
bundle_graph.remove_node(*bundle_id);
}
}
}
}
這里簡單對代碼邏輯做個講解,其實(shí)這里的邏輯也比較好理解:
- 先找到對應(yīng)的 root_bundle(即這里的 bundle_group_id ),因?yàn)橐话阒挥?nbsp;root_bundle 會有多個 bundle 并行請求。
- 找出包括 root_bundle (不包括 root_bundle)依賴的所有 bundle(即 neighbors 變量),如果這個數(shù)目大于這里設(shè)置的限制(即 limit),按照neighbors 中的所有 bundle 體積大小,把體積小的 bundle 都合并到其對應(yīng)的 source_bundles 中去(這里不一定只有 root_bundle,還可能會有其他的 bundle)。一直合并到這些 bundle 的數(shù)目小于請求限制即可。
- 合并到最后,如果只剩下唯一一個 bundle 了,那么直接把這個 bundle 也給合并進(jìn)剩下的 source_bundle 中去。
總結(jié)
在走完最后兩步的 bundle 合并之后,那么整個 Rust Demo 的打包算法流程就結(jié)束了,實(shí)際上這個 Demo 給的資源樣例并沒有走最后的兩步:
圖片
按照最后的 bundle_graph 生成的圖我們可以看到:
- 最小的 common_bundle 體積也要大于 10
- 最大的并行請求數(shù)目也并沒有超過 3 個(圖中的幾個 bundle 之間還有依賴關(guān)系,這里并沒有標(biāo)注出來)
實(shí)際上最后生成的整體 bundle 也大概如圖所示。
不過這整個流程由于筆者對于 Rust 代碼并不是很熟悉,因此中間可能會有些疏漏,不過整體流程來看應(yīng)該并沒有特別大的出入。