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 的内存使用和性能。