您的 Next.js 捆绑包将感谢您
您的 Next.js 捆绑包将感谢您
Picture by Marek Piwnicki on Unsplash
如果您的 Next.js 应用程序的包大小非常大,那么本文可能是您的救命稻草。
前言
在上一个时期,我不得不接触一个使用 Next.js 制作的项目,要求提高它的性能,因为未知原因,一切似乎都非常缓慢。
尽管本文中除了包大小(Missing Image Optimization,Bad Caching Policies)之外还有其他问题,但我将只关注包大小引起的问题。
初步检查
我做了一些检查,运行了几份 Lighthouse 报告,结果平均性能得分为 35 分 在移动设备和台式机上。实际上,他们没有错,存在一些问题。在快速检查报告后,我开始进行另一种类型的测试,启动生产版本来检查 Next 提供给我们的漂亮报告。结果让我 从我的椅子上跳下来 .
为了说明这一点,让我们从正确的事情开始,下面是一个完全可以接受的中小型下一个应用程序的构建。 (其实我的 网站 )。
如您所见,First Load JS 小于 100kB,因此您漂亮的终端将显示为令人振奋的绿色。
然而,在我告诉你的情况下,输出却完全不同。只是稍微大一点……
分析问题
为了更好地帮助您理解和测试,我准备了一个演示项目,其中包含与我一直在研究的问题类似的问题,以便您可以以实用的方式帮助您了解如何解决这些问题。您将看到的数据和测量与此演示项目有关。你可以看看所有 源代码 ,它真的很小,所以需要很短的时间。
这是被指控的应用程序的生产版本的输出:
你不觉得很麻烦吗?就个人而言,看到这些东西对我来说有点害怕。现在让我们分析这个输出,并提出一些想法来复活这个应用程序。
一些快速说明可以帮助您更好地理解这里的整体问题:
所有文件共享的JS
如您所见,底部显示了一个部分,其中指定了所有底层代码如何由每个生成的 API 和 Pages 块继承。
这是什么意思?好吧,例如,/signin 页面的 First Load JS 为 303kB,但公共部分的重量为 109kB,这意味着该页面上使用的模块的实际重量为 194kB。
计算中不包括 CSS
这对许多人来说可能很明显,但值得指出的是,对于那些可能是新手的人来说,您在底部看到的任何 CSS 都不包括在计算中。这并不是说它不会导致可能的问题,而是与另一种类型的问题有关。
所以,让我们从看似显而易见的事情开始,怎么可能所有页面 大小相同 ?这很奇怪,查看来源所有页面都有不同的导入,因此它们应该有不同的大小对吗?同样有趣的是 _应用程序 是 相对较小 ,所以不会对这些巨大的数字产生太大影响。
我们可以做的一件事是尝试分析我们的生产包,看看它对我们说了什么,我们可以使用一个非常好的工具来进行包分析,称为 Next.js 捆绑分析器 它很容易安装(所以我会跳过那部分),它会给你一个关于所有包大小的漂亮的交互式热图。
这是构建的依赖关系热图,如果您下载了源代码,您也可以使用 ANALYZE=true npm 运行构建
:
如果你从来没有见过这样的图表,它可能看起来很复杂,其实概念很简单,最大的窗格最重,窗格的内容是对应最大窗格中包含的源代码。
那么,快速看一下图表,您认为问题出在哪里 实际上,这里有两个主要问题,让我们一一深入探讨。
第一个问题 对于通常不做这类事情的人来说可能很难发现,但很快就会变得非常明显:有 一大块 包含所有依赖项!
是的,我说的是左边的那块,你知道吗?这将与所有将要导入的页面共享 至少其中一个依赖项,即使后者很小 !认为可以 有用的提示 关于为什么所有页面都具有相同的大小!
第二个 相反,可能更容易发现,这个应用程序正在使用一些巨大的依赖关系,第一个跳出来的是:
- @mui/x-data-grid
- 形容词
- 反应电话输入标记
在了解这些依赖项的名称后,我们可以采取的一项很好的操作是快速查看我们的源代码,了解它们的使用位置和使用量。在我们的例子中,如果你愿意,你也可以这样做,但如果没有,好消息,我已经为你完成了工作,结果是:
- @mui/x-data-grid 用于
随机表.js
组件,而这又仅用于表.js
页。 - “ajv”包非常相似,它用于
auth-form.js
这反过来又只在使用的组件登录.js
页。 - 最后一个用于
电话输入
, 但后者 未使用 在任何页面上!!!
现在,知道是什么导致了问题,回顾一下之前完成的构建输出。 WTF在这里发生!?
桶文件的神奇疯狂
为了清楚起见,什么是桶形文件?那么,你知道你什么时候把你所有的出口都放在一个 index.js
文件有更容易的导入路径?这是一个桶文件。 (你知道 Node.js 的创建者 后悔创造了它 ?)
所以,我想试验一下,看看捆绑测量,我们肯定知道 形容词
是一个严重的依赖,所以我要打开 登录.js
页面,我将评论 AuthForm 组件。
太好了,我迫不及待地想重做一个,看看我减轻了多少重量!所以我重新启动了一个版本,然后……“Happy Music Stops”,没有任何改变。这 登录.js
页 仍然是 303kB ...
在歇斯底里,我决定尝试一切,所以我也评论了 导航栏
组件,会发生一些事情,不是吗?
在下一次构建中 神奇的事情发生了 :
现在页面已经失去了所有的重量!这怎么可能?是不是 导航栏
导致一切的组件?尝试像以前一样把所有东西都放回去,只删除 导航栏
这次的组件。有什么改变吗?
让我猜猜,对吧?因此,我们确定通过单独删除这两个组件,问题仍然存在,但是当我们神奇地删除它们时,一切都消失了。
现在我想让你知道我一直隐藏的一个小秘密,这样你就可以推理和理解发生了什么,我会通过最后一次测试让你知道,让我们尝试像这样更改导入:
让我们启动另一个构建,现在发生了什么?
重量似乎减轻了很多,当然,它仍然很大,因为让我们记住 认证表格
正在使用重度依赖,但是之前和现在之间的实质性区别是什么?
如果您在 components 文件夹中注意到,有一个看似无害的文件,迄今为止从未提及 index.js:
想一想这个文件中发生了什么,这个文件负责导出 成分
文件夹,使它们可以使用更轻的导入语法。我将在这里粘贴两个导入版本之间的差异:
所以是的,我们保存了一个子目录,但结果是什么?从这里导入一个很小的模块就足够了 index.js
导致页面包中所有其他组件的大量导入。
这就是为什么我们只删除了两者之间的两个组件之一的原因 认证表格
和 导航栏
结果没有改变,因为两者都造成了同样巨大的进口效应。仅通过删除它们两个,文件的引用 index.js
丢失,因此没有导入任何组件!
作为继续之前的最后测试,让我们替换每个页面上的所有这些导入并启动另一个构建:
现在一切似乎更加一致,考虑到它使用的模块,每个页面似乎都有正确的大小。
实际上, 这并不奇怪 那个 表.js
页面更大,因为查看热图我们知道@mui/x-data-grid 肯定很重。
为什么会这样?
到目前为止,我们已经发现并解决了所有页面都包含一个由未使用的依赖项组成的巨大捆绑包的问题,我们看到这是因为从 index.js
文件,现在我想解释一下为什么这个东西会导致这个问题。
通常,在生产构建中,JavaScript 代码会经历包括删除未使用模块在内的多个操作,这种特定现象称为 摇树 .你可以这样想象:你的花园里有一棵漂亮的树,那棵树是你的应用程序源代码,绿色和健康的叶子是你的应用程序使用的模块,而棕色和几乎枯死的叶子是未使用的模块。
现在想象一下用你所有的力量摇晃这棵树 让枯叶落到地上,只留下健康的叶子 .这就是 Tree Shaking,在我们的例子中,它通常由模块捆绑器(如 Webpack 或 Rollup)制作。
基本上,Next.js 中的想法是,框架尝试通过创建与页面相关的块来应用代码拆分,尝试删除特定页面的所有未使用模块,以使其加载速度更快,并且无需评估无用代码。
但是在某些情况下,我们的打包程序(在这种情况下是 webpack,因为它被 Next 使用)不能自动删除一些模块。这只是因为 Terser(webpack 用于此操作的模块) 不能总是安全地确定是否使用了模块导出 .正如 webpack 文档所说:
Terser 试图弄清楚,但在许多情况下它并不确定。这并不意味着 terser 没有做好它的工作,因为它无法弄清楚。在像 JavaScript 这样的动态语言中可靠地确定它太困难了。
这是否意味着不能再使用桶文件? 可能不是 .
替代解决方案
我已经听到你的声音用类似这样的短语轰炸我的脑袋:
是的,一切都很好,但是没有办法通过保持
index.js
?
实际上,(大多数时候)为您提供了替代解决方案。
正如您刚刚读到的,Terser 做得很好,但有时它并不完美。为了让它更好地工作,我们可以给 webpack 一个很好的提示,叫做 副作用
.这个值可以放入 包.json
并接受正则表达式、字符串和布尔值作为值。 但究竟是什么副作用 ?
好吧,官方文档可以帮助我们:
“副作用”定义为在导入时执行特殊行为的代码,而不是暴露一个或多个导出。这方面的一个例子是 polyfill,它影响全局范围并且通常不提供导出。
例如在我们的例子中我们没有使用任何副作用,所以我们可以直接设置为 false,帮助 webpack 修剪未使用的模块:
现在,如果我们尝试创建一个生产版本来保留来自桶 (index.js) 文件的旧导入,看看会发生什么:
当我们将桶文件中的所有导入替换为单个导入时,我们得到了相同的结果!您还可以查看新的热图以立即发现差异。
看看现在有不同的块(不同大小),我们没有更多的大块被所有页面共享!
一个聪明的问题
你还记得我们什么时候删除了 认证表格
和 导航栏
组件从 登录.js
页?我们最初解决了这个问题,但我错了,还是仍然存在导入的依赖项?
从“@mui/material”导入{ Box };
为什么这个依赖没有继续引起另外两个的问题呢?然而这里又是一个 桶文件 用于导出所有组件,好吧,答案又可以是 在这里找到 .如果你想知道, 脉轮UI 在所有组件上使用相同的,并且 维护用户界面 也在使用它,甚至 洛达什 (在 ESM 版本中)利用了这种技术。
增强摇树的常用技巧
我认为我们可以应用很多技巧,我会在这里写一些我现在经常使用的技巧。
使用 Tree Shakeable 库
同样,这可能是陈词滥调,但很常见的是看到具有大量非可摇树依赖的项目,您如何知道其中一个是否可摇树?使用类似的工具 捆绑恐惧症 .
避免转译为 CommonJS
您应该配置您的捆绑器以保持所有 ESM 完整,而不是将其转换为 CJS,否则,从捆绑器应用摇树将更加困难。例如,您可以使用以下代码使用 Babel 进行此操作:
避免明星进口
您应该只导入您需要的模块,避免从模块中导入 *,否则所有内容都将包含在您的代码块中,即使它没有被使用!
处理巨大的依赖关系
所以,一个问题解决了,但我们还有另一个问题。让我们考虑处理那些巨大的依赖关系,选择其中一个,我通常会开始问自己一些问题:
- 这个库是必需的还是可以用其他东西代替?
- 如果我们需要这些功能,有没有更轻量级的替代品可以做同样的事情? (想想 lodash 和 lodash-es)
让我们一一挑选那些依赖关系,看看我们是否可以做一些替换,从最大的开始 @mui/x-data-grid .
快速提醒一下,正如我之前所说的项目是一个例子,它不是真实的,它是为了教学目的。根据您的要求考虑这些因素!
所以,看代码,基本上,我们正在显示一个表格,没有任何特殊需求,它只是一个用户列表。我们不关心这个网格提供给我们的任何复杂功能。而且,如果需要排序或搜索,我们绝对可以使用更轻量级的解决方案,例如 反应表 这更轻。
让我们继续谈论 形容词 ,即使在这种情况下,这里的要求是验证一个简单的表单,简单到我们甚至可以手动完成。在这种情况下,没什么好说的,如果没有障碍,最好选择与此不同的解决方案。这个图书馆的事实更加突出了这一点 不可摇树 .针对这种特殊情况的不同库? 上层建筑 可能很酷(而且更轻巧)。
最后一个最简单,实际没用过 反应电话输入标记 .这可以很容易地删除,因为它的唯一任务是增加捆绑包,但我为什么要包含它?仅仅是因为 常常 如果代码库没有得到持续维护,则可能会在各种易手和需求变化之间发生 有些东西甚至没有被使用就留在了源代码中 .因此,有时最好进行依赖性检查,然后查看是否可以删除某些内容,以节省字节和构建时间。
最后但并非最不重要的!
这是一段漫长的旅程,但我希望你们都安然无恙,如果有任何问题或只是想停下来打个招呼,你可以找到我 推特 , 或者 领英 .
另外,停下来签署我的 留言簿 让我知道你对这篇文章的看法!
我会在下面留下一些可能对你有帮助的链接!
- 摇树文档 (Webpack)
- 捆绑恐惧症
- 进口成本 对于 VS 代码
- 我的网站!
版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明