mirror of
https://github.com/hanzoai/next.js.git
synced 2026-09-24 17:00:51 +00:00
This fixes memory leaks caused by `prevExports` in react-refresh-utils.
It happens in code like the following:
```tsx
const DATA = Array.from({ length: 100000 }, (_, i) => Math.random());
export const App = () => {
return (
<div>
<div>REWRITE_HERE</div>
<div>{DATA.length}</div>
</div>
);
};
```
After we edit this file to trigger fast refresh, previous `DATA` will be
still retained in the memory since it forms `App(new) -> prevExports ->
App(old) -> DATA` reference chain (there is some screenshots
[here](https://github.com/pmmmwh/react-refresh-webpack-plugin/pull/766)).
I believe there is no reason to retain the whole exports as
`prevExports`. We can just retain "signature" (`string[]`). By only
holding this, we no longer create reference to the old exports, which
fixes the memory leak here. Note that I filed a similar PR in
https://github.com/pmmmwh/react-refresh-webpack-plugin/pull/766 and also
https://github.com/naruaway-sandbox/fast-refresh-hmr-memory-leak-demo is
a reproducible example of this issue, which also explains that
interestingly this issue is not easily solved for Vite.
## Should we fix it?
I think yes, as long as there is no unintended side effect, it's better
to fix it since we cannot predict whether users would load large payload
AND does Fast Refresh many times without reloading the browser or not.
In [this extreme
case](https://github.com/naruaway-sandbox/fast-refresh-hmr-memory-leak-demo),
it eats several hundred mega bytes of RAM.
## Verification
I confirmed that the memory leak is gone with this change by running
https://github.com/naruaway-sandbox/fast-refresh-hmr-memory-leak-demo
with the change.
I am not sure whether new tests are needed but my concern is to
accidentally break Fast Refresh behavior somehow. I believe we have
enough existing test cases 🙏 and I also tested manually.
92 lines
3.7 KiB
TypeScript
92 lines
3.7 KiB
TypeScript
import { RefreshRuntimeGlobals } from '../runtime'
|
|
|
|
declare const self: Window & RefreshRuntimeGlobals
|
|
|
|
type Dictionary = { [key: string]: unknown }
|
|
declare const __webpack_module__: {
|
|
id: string
|
|
exports: unknown
|
|
hot: {
|
|
accept: () => void
|
|
dispose: (onDispose: (data: Dictionary) => void) => void
|
|
invalidate: () => void
|
|
data?: Dictionary
|
|
}
|
|
}
|
|
|
|
// This function gets unwrapped into global scope, which is why we don't invert
|
|
// if-blocks. Also, you cannot use `return`.
|
|
export default function () {
|
|
// Wrapped in an IIFE to avoid polluting the global scope
|
|
;(function () {
|
|
// Legacy CSS implementations will `eval` browser code in a Node.js context
|
|
// to extract CSS. For backwards compatibility, we need to check we're in a
|
|
// browser context before continuing.
|
|
if (
|
|
typeof self !== 'undefined' &&
|
|
// AMP / No-JS mode does not inject these helpers:
|
|
'$RefreshHelpers$' in self
|
|
) {
|
|
// @ts-ignore __webpack_module__ is global
|
|
var currentExports = __webpack_module__.exports
|
|
// @ts-ignore __webpack_module__ is global
|
|
var prevSignature: unknown[] | null =
|
|
__webpack_module__.hot.data?.prevSignature ?? null
|
|
|
|
// This cannot happen in MainTemplate because the exports mismatch between
|
|
// templating and execution.
|
|
self.$RefreshHelpers$.registerExportsForReactRefresh(
|
|
currentExports,
|
|
__webpack_module__.id
|
|
)
|
|
|
|
// A module can be accepted automatically based on its exports, e.g. when
|
|
// it is a Refresh Boundary.
|
|
if (self.$RefreshHelpers$.isReactRefreshBoundary(currentExports)) {
|
|
// Save the previous exports signature on update so we can compare the boundary
|
|
// signatures. We avoid saving exports themselves since it causes memory leaks (https://github.com/vercel/next.js/pull/53797)
|
|
__webpack_module__.hot.dispose(function (data) {
|
|
data.prevSignature =
|
|
self.$RefreshHelpers$.getRefreshBoundarySignature(currentExports)
|
|
})
|
|
// Unconditionally accept an update to this module, we'll check if it's
|
|
// still a Refresh Boundary later.
|
|
// @ts-ignore importMeta is replaced in the loader
|
|
global.importMeta.webpackHot.accept()
|
|
|
|
// This field is set when the previous version of this module was a
|
|
// Refresh Boundary, letting us know we need to check for invalidation or
|
|
// enqueue an update.
|
|
if (prevSignature !== null) {
|
|
// A boundary can become ineligible if its exports are incompatible
|
|
// with the previous exports.
|
|
//
|
|
// For example, if you add/remove/change exports, we'll want to
|
|
// re-execute the importing modules, and force those components to
|
|
// re-render. Similarly, if you convert a class component to a
|
|
// function, we want to invalidate the boundary.
|
|
if (
|
|
self.$RefreshHelpers$.shouldInvalidateReactRefreshBoundary(
|
|
prevSignature,
|
|
self.$RefreshHelpers$.getRefreshBoundarySignature(currentExports)
|
|
)
|
|
) {
|
|
__webpack_module__.hot.invalidate()
|
|
} else {
|
|
self.$RefreshHelpers$.scheduleUpdate()
|
|
}
|
|
}
|
|
} else {
|
|
// Since we just executed the code for the module, it's possible that the
|
|
// new exports made it ineligible for being a boundary.
|
|
// We only care about the case when we were _previously_ a boundary,
|
|
// because we already accepted this update (accidental side effect).
|
|
var isNoLongerABoundary = prevSignature !== null
|
|
if (isNoLongerABoundary) {
|
|
__webpack_module__.hot.invalidate()
|
|
}
|
|
}
|
|
}
|
|
})()
|
|
}
|