Repository: vercel/next.js
Pull request: fix: cacheMaxMemorySize should not disable dev HMR cache

When configuring `cacheMaxMemorySize` to disable memory caching (typically used for setups to adjust production caching behavior, especially in multi-process self-hosted setups), this inadvertently also disabled dev HMR caches which are intended as a performance optimization. As a result of this, the dev console would be spammed with "Single item size exceeds maxsize". 

It's still possible to use this configuration to set a larger cache, but otherwise it will have a minimum set to the previous default.

Fixes NAR-463
x-ref: https://github.com/vercel/next.js/pull/85153#issuecomment-3429636751

Changed files:
filename: packages/next/src/server/dev/next-dev-server.ts
patch: @@ -48,6 +48,7 @@ import { loadDefaultErrorComponents } from '../load-default-error-components'
 import { DecodeError, MiddlewareNotFoundError } from '../../shared/lib/utils'
 import * as Log from '../../build/output/log'
 import isError, { getProperError } from '../../lib/is-error'
+import { defaultConfig } from '../config-shared'
 import { isMiddlewareFile } from '../../build/utils'
 import { formatServerError } from '../../lib/format-server-error'
 import { DevRouteMatcherManager } from '../route-matcher-managers/dev-route-matcher-manager'
@@ -185,8 +186,14 @@ export default class DevServer extends Server {
     this.appDir = appDir
 
     if (this.nextConfig.experimental.serverComponentsHmrCache) {
-      this.serverComponentsHmrCache = new LRUCache(
+      // Ensure HMR cache has a minimum size equal to the default cacheMaxMemorySize,
+      // but allow it to grow if the user has configured a larger value.
+      const hmrCacheSize = Math.max(
         this.nextConfig.cacheMaxMemorySize,
+        defaultConfig.cacheMaxMemorySize
+      )
+      this.serverComponentsHmrCache = new LRUCache(
+        hmrCacheSize,
         function length(value) {
           return JSON.stringify(value).length
         }

filename: test/development/app-dir/server-components-hmr-cache/next.config.js
patch: @@ -2,6 +2,7 @@
  * @type {import('next').NextConfig}
  */
 const nextConfig = {
+  // cacheMaxMemorySize: 0,
   experimental: {
     // serverComponentsHmrCache: false,
   },

filename: test/development/app-dir/server-components-hmr-cache/server-components-hmr-cache.test.ts
patch: @@ -91,6 +91,61 @@ describe('server-components-hmr-cache', () => {
       })
     })
 
+    describe('with cacheMaxMemorySize set to 0', () => {
+      beforeAll(async () => {
+        await next.patchFile('next.config.js', (content) =>
+          content.replace('// cacheMaxMemorySize: 0,', 'cacheMaxMemorySize: 0,')
+        )
+      })
+
+      afterAll(async () => {
+        await next.patchFile('next.config.js', (content) =>
+          content.replace('cacheMaxMemorySize: 0,', '// cacheMaxMemorySize: 0,')
+        )
+      })
+
+      it('should not warn about "Single item size exceeds maxSize"', async () => {
+        const initialOutputLength = next.cliOutput.length
+        const browser = await next.browser(`/${runtime}`)
+        await browser.elementById('value').text()
+
+        await next.patchFile(
+          'components/shared-page.tsx',
+          (content) => content.replace('foo', 'bar'),
+          async () => {
+            await retry(async () => {
+              const updatedContent = await browser.elementById('content').text()
+              expect(updatedContent).toBe('bar')
+            }, 5000)
+
+            // Verify the warning does not appear
+            const newOutput = next.cliOutput.slice(initialOutputLength)
+            expect(newOutput).not.toContain('Single item size exceeds maxSize')
+          }
+        )
+      })
+
+      it('should still use cached fetch calls for fast refresh requests', async () => {
+        const browser = await next.browser(`/${runtime}`)
+        const valueBeforePatch = await browser.elementById('value').text()
+
+        await next.patchFile(
+          'components/shared-page.tsx',
+          (content) => content.replace('foo', 'bar'),
+          async () => {
+            await retry(async () => {
+              const updatedContent = await browser.elementById('content').text()
+              expect(updatedContent).toBe('bar')
+            }, 5000)
+
+            // HMR cache should still work even with cacheMaxMemorySize: 0
+            const valueAfterPatch = await browser.elementById('value').text()
+            expect(valueBeforePatch).toEqual(valueAfterPatch)
+          }
+        )
+      })
+    })
+
     describe('with experimental.serverComponentsHmrCache disabled', () => {
       beforeAll(async () => {
         await next.patchFile('next.config.js', (content) =>