2019年2月19日,星期二
Next.js 8 Webpack 内存改进
发布者最近,Next.js 8 发布了。此版本包括大幅减少构建时内存使用量。这篇博文将探讨我们如何帮助社区优化 webpack。
Next.js 是零配置的,构建于 webpack 和 Babel 等工具之上。 它的目标是帮助您专注于重要的事项:您的应用程序代码。
现代 Web 应用程序由一个或多个页面组成。 例如,首页、博客、仪表板或产品列表。
使用 Next.js,这些页面将成为项目根目录中特殊 pages
目录中的文件。
例如:文件 pages/about.js
映射到 URL /about
。
该框架的关键设计约束之一是它必须适用于单页和数千页。
在实现 Serverless Next.js 时,很快就明显发现,在包含数百个页面的项目上运行 next build
会导致高内存使用率。 有时会超过 Node.js 的大约 1.4 GB 内存堆限制。
我们开始使用 Chrome 开发者工具分析构建过程的内存使用情况。
在生成的配置文件中,我们发现 webpack 会一次性分配 548 MB 内存。
分配的内存量与页面数量直接相关,这意味着页面越多,内存使用量就越大。

通过查看内存配置文件的堆栈跟踪,我们能够找到导致内存分配峰值的函数。
分配本身来自 source.source()
方法的调用,该方法生成结果文件并将其存储到内存中。
但是,通过进一步查看调用 source()
方法的函数,您可以看到 compilation.assets
正在使用 asyncLib.forEach
进行迭代。 这意味着 提供的函数 将同时为 compilation.assets
数组中的每个文件调用。
因此,这意味着,例如,如果有 100 个页面,并且每个页面都必须写入磁盘,则上面的代码将尝试同时写入所有 100 个页面,包括同时生成所有 100 个文件。
此问题的解决方案是使用信号量来限制并发写入的数量。 通常我们为此使用 async-sema,但在这种情况下,webpack 已经在 neo-async 上提供了一种合适的方法。
asyncLib.forEach(compilation.assets, (source, file, callback) => {
// etc
});
先前代码同时为所有资源并发运行该函数
asyncLib.forEachLimit(compilation.assets, 15, (source, file, callback) => {
// etc
});
新代码一次最多并发运行 15 个函数
在实施此并发限制并再次分析构建内存使用情况后。 我们可以看到内存分配被分成更小的 34 MB 片段。

此更改显示了非常有希望的结果,但实际上构建仍然耗尽内存,因此我们继续分析和调查该问题。
通过进一步检查内存配置文件,我们注意到在调用 source.source()
方法后,内存没有在之后被清理(垃圾回收)。
在 webpack 中,资源通常是 Source 类 的实例。 这些类都实现了一个 source()
方法,该方法将生成文件源。
配置文件显示,许多资源是 CachedSource
的实例。 CachedSource
的工作方式是,当调用 source()
时,结果会缓存在内存中,直到资源被释放。
检查 Next.js 使用的 webpack 插件表明,在我们没有插件在 webpack 写入文件后调用 source()
,这意味着缓存写入的值没有任何好处。
在与 Tobias Koppers 合作 后,他实现了一个名为 output.futureEmitAssets
的新选项,该选项允许选择加入新的资源写入行为。
使用这种新行为,随时间分配的块减少到 182 KB。

Next.js 8 已经内置了所有这些优化。 使用 Next.js 时无需进行任何更改。
此优化已在 webpack 上引入,这意味着不仅 Next.js 用户,而且所有 webpack 用户都将受益于这些优化。
我们将积极继续改进 Next.js 和 webpack 的内存使用率和性能。