Architecture Overview
Server Runtime
Rendering Pipeline
Client Navigation
Caching and Export
Development Tools
Build and Configuration
Ecosystem Packages
Testing Infrastructure
How It Works
The following files were used as context for generating this wiki page:
React Refresh Support (commonly known as Fast Refresh) in Next.js provides instant feedback during local development by updating React components in the browser without losing component state. The system bridges lower-level Hot Module Replacement (HMR) mechanisms provided by Webpack and Rspack with the official react-refresh runtime library. By instrumenting module execution, registering React component families, and tracking exports signatures, the framework can surgically refresh updated components while falling back to full reloads when signature changes or side effects violate safety invariants.
The subsystem consists of three major structural pillars: runtime integration utilities (@next/react-refresh-utils), bundler-specific plugins (ReactFreshWebpackPlugin for Webpack and ReactRefreshRspackPlugin for Rspack), and client-side HMR reconciliation handlers (hot-reloader-app.tsx, hot-reloader-pages.ts). These layers cooperate to intercept module execution, evaluate whether exported entities qualify as React refresh boundaries, schedule updates via module.hot, and communicate status changes to the developer overlay.
The ReactFreshWebpackPlugin handles Webpack 4 and Webpack 5 integrations by hooking into compilation lifecycles to inject necessary runtime helpers and intercept module execution. For Webpack 5, the plugin appends a ReactRefreshRuntimeModule to runtime requirements via compilation.hooks.additionalTreeRuntimeRequirements.
This runtime module hooks into RuntimeGlobals.interceptModuleExecution to wrap original module factories.
When a module executes, the wrapper establishes execution interception via self.$RefreshInterceptModuleExecution$(moduleId). This temporary hook re-binds self.$RefreshReg$ and self.$RefreshSig$ to register component types and transform signature functions against the specific webpackModuleId. A try...finally block guarantees cleanup even if module execution throws an error.
// Example wiring in Webpack 5 compilation hook
compiler.hooks.compilation.tap('ReactFreshWebpackPlugin', (compilation) => {
injectRefreshFunctions(compilation, Template)
compilation.hooks.additionalTreeRuntimeRequirements.tap(
'ReactFreshWebpackPlugin',
(chunk: any) => {
compilation.addRuntimeModule(chunk, new ReactRefreshRuntimeModule())
}
)
})Note
Webpack 4 lacks a native module execution interception API, so ReactFreshWebpackPlugin inspects and rewrites the source code of the template's require/evaluation block using string matching on modules[moduleId].call(.
For Rspack environments, ReactRefreshRspackPlugin injects $ReactRefreshRuntime$ globally via Rspack's ProvidePlugin and ensures RuntimeGlobals.moduleCache is present in the compilation's runtime requirements.
The companion runtime module (rspack-runtime.ts) initializes react-refresh/runtime against self using RefreshRuntime.injectIntoGlobalHook(self), while assigning stub functions for $RefreshSig$ and $RefreshReg$ to prevent reference errors in uninstrumented modules.
The @next/react-refresh-utils loader (loader.ts) appends module-level refresh execution logic to compiled .ts, .tsx, and .js files. It embeds ReactRefreshModule.runtime.ts, adapting global.importMeta to import.meta or module.hot depending on whether the file is CommonJS or ES module syntax.
The loader processes source files by calling this.callback with the original source combined with the un-wrapped runtime code.
The injected module runtime checks whether self.$RefreshHelpers$ is available. If present, it retrieves __webpack_module__.exports and compares the previous module signature (__webpack_module__.hot.data?.prevSignature) against the current signature.
// Runtime module injection sequence
var currentExports = __webpack_module__.exports
var prevSignature = __webpack_module__.hot.data?.prevSignature ?? null
self.$RefreshHelpers$.registerExportsForReactRefresh(currentExports, __webpack_module__.id)
if (self.$RefreshHelpers$.isReactRefreshBoundary(currentExports)) {
__webpack_module__.hot.dispose(function (data) {
data.prevSignature = self.$RefreshHelpers$.getRefreshBoundarySignature(currentExports)
})
global.importMeta.webpackHot.accept()
}Helpers in packages/react-refresh-utils/internal/helpers.ts determine whether a module constitutes a valid React Refresh boundary via isReactRefreshBoundary(). A module is a boundary if it exports components likely to be React components or if all its non-safe exports qualify as component types.
Safe exports (__esModule, __N_SSG, __N_SSP, and config) are ignored during inspection.
When an update occurs, shouldInvalidateReactRefreshBoundary() compares signature arrays. If the length or any element differs between prevSignature and nextSignature, the boundary is invalidated via webpackHot.invalidate(), triggering a wider reload cascade.
Otherwise, scheduleUpdate() batches updates for execution via RefreshRuntime.performReactRefresh().
Caution
If a file exports both React components and non-component utilities consumed outside the React tree, editing it will cause Fast Refresh to trigger a full page reload (REACT_REFRESH_FULL_RELOAD).
Client entrypoints (hot-reloader-app.tsx and hot-reloader-pages.ts) manage communication with the HMR server, monitor compilation hashes against __webpack_hash__, and apply updates.
When updates arrive, tryApplyUpdatesWebpack() verifies that module.hot.status() === 'idle'. If updates cannot be applied immediately, afterApplyUpdates() registers a status handler to defer execution until the HMR status returns to 'idle'.
If runtime errors occur (RuntimeErrorHandler.hadRuntimeError), or if update application fails, performFullReload() logs stack traces and calls window.location.reload().
// Webpack hot update application flow
function tryApplyUpdatesWebpack(sendMessage: (message: string) => void) {
if (!isUpdateAvailable() || !canApplyUpdates()) {
resolvePendingHotUpdateWebpack()
dispatcher.onBuildOk()
return
}
module.hot.check(true, function(err, updatedModules) {
if (err || RuntimeErrorHandler.hadRuntimeError || updatedModules == null) {
performFullReload(err, sendMessage)
return
}
dispatcher.onRefresh()
})
}HotReloaderRspack extends HotReloaderWebpack to solve issues with Rspack's persistent caching model. While Webpack updates modules incrementally, Rspack operates on complete module graph snapshots.
To prevent Rspack from purging the module graph on server restart, HotReloaderRspack maintains a built-entries.json cache file under the dist/cache/rspack/ directory.
During afterCompile, built entry files are read from cache, verified for existence and content hash changes via calculateFileHash(), and restored into the active compiler's entry map.
// Calculating file hash for Rspack persistent entry cache validation
async function calculateFileHash(
filePath: string,
algorithm: string = 'sha256'
): Promise<string | undefined> {
if (!(await fs.access(filePath).then(() => true, () => false))) {
return
}
const fileBuffer = await fs.readFile(filePath)
const hash = createHash(algorithm)
hash.update(fileBuffer)
return hash.digest('hex')
}Warning
If a file listed in built-entries.json has been modified or deleted on disk since the last server stop, its hash validation will fail, causing the entry to be dropped from the restored cache to maintain graph integrity.