我们如何将初始 JS/CSS 大小减少 67%

2025-05-24

我们如何将初始 JS/CSS 大小减少 67%

我们一直在努力减少发送给所有 Fider 用户的字节数。作为一款使用 React 构建的 Web 应用,我们专注于 JS 和 CSS。在这篇文章中,我们将分享我们的经验、一些概念以及一些建议,帮助您在 Web 应用中实现同样的效果。

Fider 的前端基于 React 和 Webpack 构建,因此以下主题主要针对使用相同技术栈的团队,但这些概念也适用于其他技术栈。它也是一个开源项目,因此您可以查看 Pull 请求和源代码:https://github.com/getfider/fider

目录

Webpack 捆绑分析器

webpack-bundle-analyzer是一个 webpack 插件,它可以生成一个可缩放的交互式树形图,包含所有 bundles。这对于我们了解每个 bundles 中包含哪些模块至关重要。你还可以查看每个 bundles 中最大的模块。

如果您不知道根本原因,您如何解决它?

这是该插件将为您生成的一个示例。

你注意到供应商包里那个巨大的entities.json文件了吗?这是一个很好的起点,可以用来分析包的内容。

使用内容哈希的长期缓存

长期缓存是指让浏览器将文件缓存较长时间,例如 3 个月甚至 1 年。这是一个重要的设置,可以确保回访用户无需反复下载相同的 JS/CSS 文件。

浏览器会根据文件的完整路径名来缓存文件,因此如果需要强制用户下载新版本的 bundle,则需要重命名它。幸运的是,webpack 提供了一个功能,可以动态生成 bundle 的名称,从而强制浏览器只下载新文件。

我们之前在 webpack 配置中已经使用了chunkhash很长时间。99% 需要长期缓存的情况下,最好的选择是使用contenthash,它将根据其内容生成哈希值。

这种技术不会减少 bundle 的大小,但确实有助于减少用户下载 bundle 的次数。如果 bundle 没有变化,就不要强迫用户重新下载。

要了解更多信息,请访问官方文档https://webpack.js.org/guides/caching/

通用捆绑包

将所有 NPM 软件包合并到一个单独的 bundle 中,这已成为许多团队长期以来的惯例。与长期缓存结合使用时,这非常有用。

NPM 软件包的变更频率比我们的应用代码低,因此,如果没有任何变更,我们无需强制用户下载所有 NPM 软件包。这些软件包通常被称为vendor bundle

但我们可以更进一步地实践这一做法。

那么你自己的代码呢?它们也不太经常更改。也许你有一些基本组件,比如 Button、Grid、Toggle 等等,这些组件是之前创建的,并且已经有一段时间没有更改了。

这是一个很好的选择,可以用作通用 bundle。你可以查看这个PR #636,我们基本上把所有我们自己开发的模块都放在一些特定的文件夹中,然后放到了一个通用的 bundle 中。

这将确保除非我们更改基本组件,否则用户无需重新下载它。

路由级别的代码拆分

代码拆分是当前的热门话题。它已经存在了一段时间,但相关的工具和框架也已经发展了很多,使得代码拆分现在变得简单得多。

应用程序推送一个包含渲染应用程序内任何页面所需的所有 JS/CSS 的大包是很常见的,即使用户只查看主页。我们不知道用户是否会访问站点设置页面,但我们已经推送了该页面的所有代码。Fider 长期以来一直采用这种方式,现在我们进行了修改。

代码拆分的理念是生成多个较小的 bundles(通常每个路由一个),以及一个主 bundles。我们发送给所有用户的唯一 bundles 就是主 bundles,它会异步下载所有需要的 bundles 来渲染当前页面。

这看起来很复杂,但多亏了 React 和 Webpack,这不再是火箭科学了。对于使用 React 16.5 及以下版本的用户,我们推荐使用react-loadable。如果您已经使用 React 16.6,那么您可以使用 React.lazy() ,它是此版本新增的功能。

  • 在此 PR 中,您可以了解@cfilby(谢谢!)如何使用 react-loadable 为 Fider 添加代码拆分:PR #596
  • 迁移到 React 16.6 后,我们用 React.lazy 和 Suspense 替换了这个外部包:PR #646

我们还遇到了一些罕见的情况,用户在下载异步包时遇到了问题。一个潜在的解决方案已记录在“当 React lazy 失败时如何重试”中。

12 月 4 日编辑:您可能还考虑按照Anton 的评论使用可加载的

按需加载外部依赖项

通过使用 Webpack Bundle Analyzer,我们注意到我们的 vendor bundle 包含了 react-toastify 的所有内容,这是我们使用的 Toaster 库。这通常没问题,但 95% 的 Fider 用户永远不会看到 Toaster 消息。我们很少在任何地方显示 Toaster,所以如果用户不需要,我们为什么要向他们推送 30kB 的 JavaScript 代码呢

这个问题和上面的类似,只不过我们讨论的不再是路由,而是多路由中使用的功能。你能在功能级别进行代码拆分吗?

是的,你可以!

简而言之,您要做的就是从静态导入切换到动态导入。

// before
import { toast } from "./toastify";
toast("Hello World");

// after
import("./toastify").then(module => {
  module.toast("Hello World");
});
Enter fullscreen mode Exit fullscreen mode

Webpack 会将 toastify 模块及其所有 NPM 依赖项分别打包。这样,浏览器只会在需要 toast 时下载该包。如果你配置了长期缓存,那么在第二次调用 toast 时就无需再次下载。

下面的视频展示了它在浏览器上的样子。

您可以在PR #645上查看有关如何实现此操作的详细信息

Font Awesome 和 Tree Shaking

Tree Shaking 是指从模块中仅导入所需内容并丢弃剩余内容的过程。在生产模式下运行 webpack 时,此功能默认启用。

使用 Font Awesome 的通常方法是导入一个外部字体文件和一个 CSS,并将该字体上的每个字符(图标)映射到一个 CSS 类。结果是,即使我们只使用图标 A、B 和 C,也强制浏览器下载这个外部字体和一个包含 600 多个图标的 CSS 定义。

值得庆幸的是,我们找到了react-icons,这是一个 NPM 包,包含所有免费的 Font Awesome(以及其他图标包!),采用 SVG 格式,并以 ES 模块格式导出为 React 组件。

然后,您可以只导入所需的图标,Webpack 将从包中删除所有其他图标。结果如何?我们的 CSS 现在减少了约 68kB。更不用说我们不再需要下载外部字体了。这项更改是减少 Fider 上 CSS 大小的最大贡献者。

想看看怎么做?看看这个PR #631

从大型 NPM 包切换到小型 NPM 包

NPM 就像一个装满积木的乐高商店,你可以随意挑选你喜欢的。你不需要为安装的软件包付费,但你的用户需要为它添加到你的应用程序中的字节大小付费。请谨慎选择。 - @goenning

在使用 Bundle Analyzer 时,我们发现仅 Markdown-it 就消耗了我们供应商包的约 40%。因此,我们决定在 NPM 上寻找其他 Markdown 解析器。目标是找到一个更小、维护良好且包含我们所需所有功能的软件包。

我们一直在使用bundlephobia.com在安装任何 NPM 软件包之前分析其字节大小。我们已将 markdown-it 切换为 marked,这在 API 改动极小的情况下,比供应商软件包减少了约 63kB

好奇吗?查看PR #643

您还可以在 bundlephobia 上比较这两个包:

添加大型软件包之前请三思。你真的需要它吗?你的团队能否实现一个更简单的替代方案?如果没有,你能找到另一个能以更少的字节完成相同工作的软件包吗?最终,你仍然可以添加 NPM 软件包并异步加载它,就像我们上面提到的 react-toastify 一样。

优化主包至关重要

假设你有一个应用程序按路由进行代码拆分。它已经在生产环境中运行,你提交了对 Dashboard 路由组件的更改。你可能会认为 Webpack 只会为包含 Dashboard 路由的 bundle 生成一个不同的文件,对吗?

但事实并非如此。

如果应用程序中的其他内容发生更改, Webpack 将始终重新生成主捆绑包。原因是主捆绑包是指向所有其他捆绑包的指针。如果另一个捆绑包的哈希值发生更改,主捆绑包必须更改其内容,使其现在指向 Dashboard 捆绑包的新哈希值。明白了吗?

因此,如果您的主包不仅包含指针,还包含许多常见组件,如按钮、切换、网格和选项卡,那么您基本上就是在强制浏览器重新下载一些未改变的内容。

使用 webpack 捆绑分析器了解主捆绑包的内容。然后,您可以应用我们上面提到的一些技巧来减小主捆绑包的大小。

TSLib(仅限 TypeScript)

将 TypeScript 代码编译为 ES5 时,TypeScript 编译器还会向输出 JavaScript 文件发送一些辅助函数。此过程可确保我们用 TypeScript 编写的代码与不支持 ES6 特性(例如类和生成器)的旧版浏览器兼容。

这些辅助函数非常小,但是当 TypeScript 文件很多时,这些辅助函数就会出现在每个使用非 ES5 代码的文件中。Webpack 将无法对其进行树状优化,最终的打包文件将包含多次相同的代码。结果呢?打包文件会稍微大一些。

幸好有一个解决方案。NPM 中有一个名为tslib的包,它包含了 TypeScript 所需的所有辅助函数。我们可以告诉编译器从 tslib 包中导入这些辅助函数,而不是直接将其输出到 JavaScript 文件中。这可以通过在tsconfig.json文件中设置importHelpers: true来实现。别忘了使用npm install tslib —save 命令安装tslib 。

就这样!

可以从捆绑包中减少的字节数取决于非 ES5 文件的数量,如果大多数组件都是类,那么在 React 应用程序上非 ES5 文件的数量可能会很多。

下一个十亿用户

您准备好迎接下一个十亿用户了吗?想想那些目前还在为低成本设备和较慢网络苦苦挣扎的潜在用户吧。

减少 bundles 的字节大小会直接影响应用程序的性能,并有助于提高应用程序的易用性。希望这篇文章能帮助您实现这一目标。

感谢您的阅读!

文章来源:https://dev.to/goenning/how-we-reduced-our-initial-jscss-size-by-67-3ac0
PREV
我在 React.js 中一直以错误的方式创建表单 🤔
NEXT
🚀 The Missing Shell Scripting Crash Course