Skip to content

Commit

Permalink
perf: use split chunks for the node server (#54988)
Browse files Browse the repository at this point in the history
This PR introduces a change in the Next.js server that should improve memory usage nicely during dev.

## How

While investigating the repro cases in #54708, @timneutkens and I noticed that the high memory usage often involved `googleapis`. Digging a bit more, I also saw that in dev, the bundle generated for a page using `googleapis` was around 80MB and that requiring it in Node.js increased memory by 160MB 🥲 and that requiring another page that also used this also increased the memory by 160MB.

The problem is that Next.js, on new navigations and hot reloads, might need to load/reload all the code required for the page *all the time*. This issue is also exacerbated by the fact that there's a nasty Node.js bug that makes it impossible to clean that memory up, leading to memory bloat and overall a pretty bad DX.

So if we can't clean this up, what can we do about it?

The change I'm introducing in this PR is that I'm changing Next.js in order to split the code you're using from `node_modules` from the code you've written in different chunks. The idea is that the heavy code you're loading from `node_modules` is only gonna be loaded once per session this time.

This should make a navigation/page reload only load the user bundle now. On my simple test case, the cost of navigation went from ~200MB to ~40MB.

A few notes on the implementation:
- The chunks for the `node_modules` are split at the module level, this is to ensure that each of the node_modules dependencies is split in the most memory efficient manner. If it was a big monolithic chunk, it would potentially be reloaded again and again whenever reloaded, leading to leakage.
- I'm guessing we could do the same for the Edge server
- the first load for a page will still be fairly heavy memory wise, there's probably something else we can do there 
- there's also an issue with the webpack require cache being flushed, whereas it could be reused

## comparisons

### navigating a page

before
<img width="284" alt="CleanShot 2023-09-04 at 21 00 46@2x" src="https://github.com/vercel/next.js/assets/11064311/44e37df8-4414-4ca1-b6bf-fb0fb11751ea">


after
<img width="392" alt="CleanShot 2023-09-04 at 20 58 53@2x" src="https://github.com/vercel/next.js/assets/11064311/46226123-a73a-4132-a99d-fb812e59df46">
  • Loading branch information
feedthejim committed Sep 5, 2023
1 parent bf3c3ce commit 9f247d9
Showing 1 changed file with 40 additions and 0 deletions.
40 changes: 40 additions & 0 deletions packages/next/src/build/webpack-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1707,6 +1707,46 @@ export default async function getBaseWebpackConfig(
| Required<webpack.Configuration>['optimization']['splitChunks']
| false => {
if (dev) {
if (isNodeServer) {
/*
In development, we want to split code that comes from `node_modules` into their own chunks.
This is because in development, we often need to reload the user bundle due to changes in the code.
To work around this, we put all the vendor code into separate chunks so that we don't need to reload them.
This is safe because the vendor code doesn't change between reloads.
*/
const extractRootNodeModule = (modulePath: string) => {
// This regex is used to extract the root node module name to be used as the chunk group name.
// example: ../../node_modules/.pnpm/next@10/foo/node_modules/bar -> next@10
const regex =
/node_modules(?:\/|\\)\.?(?:pnpm(?:\/|\\))?([^/\\]+)/
const match = modulePath.match(regex)
return match ? match[1] : null
}
return {
cacheGroups: {
// this chunk configuration gives us a separate chunk for each top level module in node_modules
// or a hashed chunk if we can't extract the module name.
vendor: {
chunks: 'all',
reuseExistingChunk: true,
test: /[\\/]node_modules[\\/]/,
minSize: 0,
name: (module: webpack.Module) => {
const moduleId = module.nameForCondition()!
const rootModule = extractRootNodeModule(moduleId)
if (rootModule) {
return `vendor-chunks/${rootModule}`
} else {
const hash = crypto.createHash('sha1').update(moduleId)
hash.update(moduleId)
return `vendor-chunks/${hash.digest('hex')}`
}
},
},
},
}
}

return false
}

Expand Down

0 comments on commit 9f247d9

Please sign in to comment.