代码分割

Parcel 开箱即用地支持零配置代码分割。这允许您将应用程序代码拆分为可以按需加载的单独捆绑包,从而减小初始捆绑包大小并加快加载时间。

代码分割由动态 import() 语法控制,其工作方式类似于普通的 import 语句,但返回一个 Promise。这意味着模块可以异步加载。

使用动态导入

#

下面的示例展示了如何使用动态导入按需加载应用程序的子页面。

pages/index.js:
import("./pages/about").then(function (page) {
// 渲染页面
page.render();
});
pages/about.js:
export function render() {
// 渲染页面
}

因为 import() 返回一个 Promise,您还可以使用 async/await 语法。

pages/index.js:
async function load() {
const page = await import("./pages/about");
// 渲染页面
page.render();
}
load();
pages/about.js:
export function render() {
// 渲染页面
}

树摇(Tree Shaking)

#

当 Parcel 可以确定您使用了动态导入模块的哪些导出时,它将从该模块中摇掉未使用的导出。这适用于静态属性访问或解构,无论是使用 await 还是 Promise .then 语法。

**注意:**对于 await 情况,不幸的是,只有当 await 未被转译时(即使用现代浏览器列表配置),未使用的导出才能被移除。

let { x: y } = await import("./b.js");
let ns = await import("./b.js");
console.log(ns.x);
import("./b.js").then((ns) => console.log(ns.x));
import("./b.js").then(({ x: y }) => console.log(y));

共享捆绑包

#

当应用程序的多个部分依赖于相同的公共模块时,它们会自动去重并拆分到一个单独的捆绑包中。这允许常用依赖项与应用程序代码并行加载,并由浏览器单独缓存。

例如,如果您的应用程序有多个页面的 <script> 标签依赖于相同的公共模块,这些模块将被拆分到一个"共享捆绑包"中。这样,如果用户从一个页面导航到另一个页面,他们只需下载该页面的新代码,而不是页面之间的公共依赖项。

home.html:
<!DOCTYPE html>
<div id="app"></div>
<script type="module" src="home.js"></script>
home.js:
import { createRoot } from "react-dom";

createRoot(app).render(<h1>首页</h1>, app);
profile.html:
<!DOCTYPE html>
<div id="app"></div>
<script type="module" src="profile.js"></script>
profile.js:
import { createRoot } from "react-dom";

createRoot(app).render(<h1>个人资料</h1>, app);

编译后的 HTML:

home.html:
<!DOCTYPE html>
<div id="app"></div>
<script type="module" src="react-dom.23f6d9.js"></script>
<script type="module" src="home.fac9ed.js"></script>
profile.html:
<!DOCTYPE html>
<div id="app"></div>
<script type="module" src="react-dom.23f6d9.js"></script>
<script type="module" src="profile.9fc67e.js"></script>

在上面的示例中,home.jsprofile.js 都依赖于 react-dom,因此它被拆分到一个单独的捆绑包中,并通过为两个 HTML 页面添加额外的 <script> 标签来并行加载。

这也适用于使用动态 import() 进行代码分割的应用程序的不同部分。两个动态导入之间共享的公共依赖项将被拆分出来,并与动态导入的模块并行加载。

配置

#

默认情况下,Parcel 仅在共享模块达到大小阈值时才创建共享捆绑包。这避免了拆分非常小的模块并创建额外的 HTTP 请求,即使在 HTTP/2 下也会有开销。如果模块低于阈值,它将在页面之间重复。

Parcel 还有最大并行请求限制,以避免浏览器一次性请求过多,如果达到此限制,将重复模块。创建共享捆绑包时,较大的模块优先于较小的模块。

默认情况下,这些参数已针对 HTTP/2 进行了调整。但是,您可以调整这些选项以提高或降低应用程序的这些参数。您可以通过在项目根目录的 package.json 中配置 @parcel/bundler-default 键来实现。

package.json:
{
"@parcel/bundler-default": {
minBundles: 1,
minBundleSize: 3000,
maxParallelRequests: 20,
},
}

可用的选项是:

http minBundles minBundleSize maxParallelRequests
1 1 30000 6
2(默认) 1 20000 25

您可以在 web.dev 上阅读更多关于这个主题的内容。

内部化异步捆绑包

#

如果一个模块在同一个捆绑包中同时被同步和异步导入,它将不会被拆分到单独的捆绑包中,而是被"内部化"。这意味着它将保留在动态导入的同一个捆绑包中以避免重复,但被包装在 Promise 中以保留语义。

因此,动态导入仅仅是一个提示,表明依赖项不是同步需要的,而不保证会创建新的捆绑包。

去重

#

如果动态导入的模块有一个依赖项在其所有可能的祖先中已经可用,则该依赖项将被去重。例如,如果一个页面导入了一个库,而该库也被动态导入的模块使用,则该库将仅包含在父级中,因为它在运行时已经在页面上。

手动共享捆绑包

#

note:手动共享捆绑包目前是实验性的,可能会发生变化。

默认情况下,Parcel 会自动将常用模块拆分为"共享捆绑包",并在上述场景中创建捆绑包。但是,在某些情况下,您可能希望精确指定捆绑包的内容以及谁可以请求该捆绑包。

这些场景包括但不限于:

手动共享捆绑包可以在项目根目录的 package.json 中指定。assets 属性必须设置为 glob 模式列表。任何匹配这些 glob 模式的资源文件路径都将包含在手动共享捆绑包中。

这个示例创建了一个供应商捆绑包,其中包含从 manual.js 开始的图中的所有 JS 资源,分成 3 个并行的 HTTP 请求。

package.json:
{
"@parcel/bundler-default": {
manualSharedBundles: [
{
name: "vendor",
root: "manual.js",
assets: ["**/*"],
types: ["js"],
split: 3,
},
],
},
}

完整的选项列表如下:

请小心!

配置手动共享捆绑包会覆盖 Parcel 通常进行的所有自动代码分割,并可能导致意外的加载顺序问题,因为它映射了整个代码库,包括 node_modules。请谨慎使用 glob,每种文件类型只指定一个捆绑包,并建议您指定一个 root 文件。