将应用程序的一部分分解到它自己的 NPM 包中会导致更大的整体应用程序大小

Posted

技术标签:

【中文标题】将应用程序的一部分分解到它自己的 NPM 包中会导致更大的整体应用程序大小【英文标题】:Factoring out part of app into its own NPM package results in larger overall app size 【发布时间】:2021-11-28 06:38:38 【问题描述】:

我们在所有项目中都使用create-react-app 和 Typescript,并且出现了一个稍大的常用 React 组件模块。试图将这些因素分解到它自己的 NPM 包中(为了更容易维护和更好地重用),这里称为 PackageA,我们已经到达了一个测试应用程序的整体大小,这里称为 TestApp,比它更大的情况之前(当相同的代码存在于同一个应用程序的代码库中时)。 TestApp 是一个非常初级的应用程序,它基本上只是展示了 PackageA 中组件的一些(但不是全部)部分,以前是项目本身内部的组件,现在删除了这部分,而是从私人发布的 @987654326 导入@。

在将 PackageA 与来自 TestApp 的组件分解之前的初始 JS 块的大小(假设“main”是来自项目本身的代码,而另一个块被认为包含依赖项):

Gzipped:“主”块 ~31 kB,具有外部依赖关系的块 ~193 kB(来自构建应用后的输出) 解压缩:“主”块 ~93 kB,具有外部依赖项的块 ~653 kB(来自浏览器)

TestApp 中分解出PackageA 后的尺寸:

Gzipped:主块 ~22 kB,具有外部依赖关系的块 ~215 kB(来自构建应用后的输出) 解压缩:主块 ~57 kB,具有外部依赖项的块 ~745 kB(来自浏览器)

可以看出,整体大小随着 13 kB gzip 和 56 kB 解压缩而增加。这 对应于 ~ 6% gzip 和 ~ 8% 解压缩。这并不算多,但我仍然希望它们有点相似。

更多信息

PackageA 的内容作为 es6 模块发布,以允许 tree-shaking,这似乎可以正常工作,因为 PackageA 的未使用部分不会发送到 TestApp 的输出块中。 UglifyJS 在发布之前使用--compress--mangle 选项对PackageA 进行缩小。 源地图不包含在PackageA 的源文件中,只能单独提供。 在PackageApackage.json 中的dependencies 下列出的唯一软件包在TestApp 的其他任何地方都没有使用,随后它们同时从TestApppackage.json 中删除添加到PackageApackage.json。使用了相同的版本。 PackageA 的所有其他依赖项都列在 peerDependencies 下。 在删除 node_modules 文件夹并运行全新安装依赖项后验证所有大小。 PackageA 的内容与TestApp 中删除的文件夹完全相同,只是添加了一个导出其他文件内容的index.ts 页面。此文件的解压缩大小为 3 kB。

尺寸增加的可能原因是什么?我们可以从哪里开始寻找?也许这种增加可能在于代码在包中的转译方式,并且create-react-app 能够以某种方式对包含在项目本身的代码比对导入的代码更有效地做到这一点。我知道这是一个棘手的问题,可能有很多答案,而且很难回答。

这是分解出的PackageA中使用的tsconfig.json


  "compilerOptions": 
    "target": "es6",
    "lib": ["es6", "dom"],
    "jsx": "react-jsx",
    "module": "es6",
    "rootDir": "./src",
    "moduleResolution": "node",
    "declaration": true,
    "sourceMap": true,
    "outDir": "./build/esm",
    "inlineSources": true,
    "allowSyntheticDefaultImports": true,
    "esModuleInterop": true,
    "forceConsistentCasingInFileNames": true,
    "strict": true,
    "skipLibCheck": true
  ,
  "include": ["./src"],
  "exclude": ["**/*.test.tsx", "**/*.test.ts", "**/*.stories.tsx"]

【问题讨论】:

【参考方案1】:

我知道这个问题可能在很大程度上取决于您所处的情况,但我仍然认为我会分享我的过程。

测量尺寸

通常在您构建时,您的框架会输出构建工件的大小。在 create-react-app 的情况下,这些大小似乎是 gzip 压缩版本的大小。

您还可以检查您的浏览器开发工具并注意下载的包的大小,这些包通常不是压缩后的大小,而是实际大小。

在考虑因素或进行某种比较时,请注意更改前后的不同尺寸。给定关于这些尺寸应该如何改变的某种直觉,评估是否是这种情况。

分析捆绑包

对于 NextJS,您可以使用@next/bundle-analyzer,在大多数其他情况下,您可以使用source-map-explorer。后者使用源映射将捆绑的代码映射到源;如果源本身有源映射,则使用这些源,否则您将看到对node_modules 的引用。使用source-map-explorer,可能值得同时研究视觉表示和 json 表示,因为有很多来源,视觉表示可能会抑制 json 表示不会参与的某个组件或库的参与。

使用source-map-explorer,您可以在应用的不同版本之间进行一些有价值的比较。

分析源代码到生产代码的特殊情况

在使用例如source-map-explorer 确定某些源代码在某些上下文中扩展得更多之后,您可能希望进一步研究该问题。使用源映射(在开发模式下或在生产模式下,如果您有它们可用),您可能想要跟踪输出包中源代码的某个部分的印记。许多浏览器支持将捆绑代码映射到源代码(给定源映射),但并非所有浏览器都允许您做相反的事情。然而,Firefox 具有此功能,它允许您在给定捆绑代码的情况下查看源代码,反之亦然。当您尝试跟踪某些源代码在输出中的实际结果时,这非常有用。

我的用例呢?

嗯,以下是在我的案例中导致尺寸增加的原因列表:

我从包中的索引导入,索引导入包的所有导出。这不应该是 esm 模块的问题,但是如果没有 package.json 中的 sideEffects: false 标志,就会出现一些歧义。即使 MY 包中未使用的源没有添加到输出包中,但不能排除一些间接依赖项没有副作用,因此将这些添加到包中,即使导入它们的组件甚至没有使用。在我的包的package.json 中添加sideEffects: false 解决了这个问题并减小了包大小。

在使用create-react-app 捆绑的主项目中保留组件时,由于@babel/plugin-transform-runtime 插件的存在,Babel 在所有转译文件中添加的一些便利方法被删除。这个插件删除了这些便利方法,因为在运行这些文件时保证存在一些 Babel 运行时(它们可以从中导入)。当分解出的组件使用相同的配置时,尺寸缩小得更多。

最后,在撰写本文时,tsc 编译器似乎输出的代码比 Babel 更冗长(请参阅 TypeScript: tsc transpiles jsx to more verbose code than babel)。转包时我第一次使用tsc,在开始使用Babel之后,尺寸更小了。

总而言之,通过上述更改,最终输出(解压缩后)仅比重构前大 1.7 kB,这是一个既可以理解又可以接受的结果。

【讨论】:

以上是关于将应用程序的一部分分解到它自己的 NPM 包中会导致更大的整体应用程序大小的主要内容,如果未能解决你的问题,请参考以下文章

Npm pack 包含本地模块

[python 并行1]简介

如何从 webpack 生成的 NPM 包中正确加载资产?

如何使用 `tempdisagg` 包中的 `td` 命令将每月数据分解为每日数据频率?

如何将打字稿定义文件添加到 npm 包中?

React Typescript NPM 包中的外部 CSS