HMR, the revolution in development experience
Introduction Hot Module Replacement (HMR) is an essential mechanism for modern developers, as it allows code to be modified in real time without interrupting the workflow. Its impact can be measured in two major ways: Faster iterations: changes are instantly visible, reducing validation cycles. Improved developer experience: no more interruptions due to manual reloads. In the Dart ecosystem, outside Flutter, traditional HMR is rare. Our implementation offers a pragmatic alternative, designed to meet specific needs while maintaining radical simplicity. Philosophy Our solution is based on three founding principles: Voluntary State Reset Unlike traditional HMR systems, which preserve state in memory, our approach completely restarts the execution context whenever a change is made. This choice, although counter-intuitive, offers key advantages: Predictable behaviour: the application always starts with a clean initial state, eliminating the risks associated with corrupted variables or accumulated side effects. Compatibility with stateless architectures Ideal for HTTP servers, CLI tools or ephemeral scripts where the external state (database, files) takes precedence. Total isolation Each reload relies on Dart isolates, independent units of execution: Greater stability: a failing isolate does not affect the main process. No dependency on the Dart VM: The code runs identically in JIT, AOT or as a native executable. Optimum memory management: isolates are destroyed cleanly after use, avoiding memory leaks. Minimalism and Performance With just 6 KB and no external dependencies, the solution stands out for : Virtually instantaneous reload time ( print('Watching for changes...'), middlewares: [ IgnoreMiddleware(['~', '.dart_tool', '.git', '.idea', '.vscode']), IncludeMiddleware([Glob("**.dart")]), DebounceMiddleware(Duration(milliseconds: 5), dateTime), ], onFileChange: (int eventType, File file) async { final action = switch (eventType) { FileSystemEvent.create => 'created', FileSystemEvent.modify => 'modified', FileSystemEvent.delete => 'deleted', FileSystemEvent.move => 'moved', _ => 'changed' }; print('File $action ${file.path}'); await runner.reload(); }); watcher.watch(); runner.run(); } How it works From change to reload It all starts with a change to the source code by the developer. This action triggers a cascade of automated steps: The Watcher, constantly monitoring the file system, instantly detects the change (creation, modification or deletion). This detection is a non-blocking event that allows the watcher to continue monitoring other files while the reload process is underway. Example: A fix is made to a calculation function. The Watcher identifies the file backup and transmits the event to the Runner in less than 2 ms. Incremental compilation Once notified, the Runner orchestrates the targeted compilation: The modified code is compiled into a .dill file, Dart's intermediate binary format, which will be deposited in a temporary folder so as not to pollute the source project. Only the modules affected by the change are recompiled, a process optimized to take less than 5 ms even on large projects. Isolate Management The heart of the approach lies in Dart isolates, lightweight, isolated execution environments: The previous instance is killed immediately, freeing up memory and preventing leaks. The new code (via .dill) is loaded into a blank context, guaranteeing a predictable initial state. If the new code contains a critical error (e.g. infinite loop), only the isolate is affected. The main process remains stable, allowing subsequent reloading. Continuous loop The cycle repeats itself with each modification, creating an immediate feedback loop: The developer saves the file. The Watcher detects the change. The Runner recompiles and relaunches the isolate. the result is visible without any noticeable delay. This loop is non-blocking, so that when an isolate is active, we can trigger a new reload if necessary. Key architectural benefits Risk Isolation Isolates function like sandboxes, preventing propagation errors. No shared dependencies: each instance is autonomous. Universal compatibility The .dill format is compatible with all Dart compilation modes (JIT, AOT, exe). No modifications are required for production deployment. This diagram illustrates the elegant simplicity of your implementation: Watcher → Responsive vigil. Runner → Efficient conductor. Isolates → Disposable and reliable workers. By combining these elements, you get a reloading system that's fast, stable and tailored to the demands of modern Dart applications, while avoiding the complexity of traditional solutions. "A mechanic doesn't have to be complicated to be powerful."
Introduction
Hot Module Replacement (HMR) is an essential mechanism for modern developers, as it allows code to be modified in real time without interrupting the workflow.
Its impact can be measured in two major ways:
- Faster iterations: changes are instantly visible, reducing validation cycles.
- Improved developer experience: no more interruptions due to manual reloads.
In the Dart ecosystem, outside Flutter, traditional HMR is rare. Our implementation offers a pragmatic alternative, designed to meet specific needs while maintaining radical simplicity.
Philosophy
Our solution is based on three founding principles:
Voluntary State Reset
Unlike traditional HMR systems, which preserve state in memory, our approach completely restarts the execution context whenever a change is made.
This choice, although counter-intuitive, offers key advantages:
- Predictable behaviour: the application always starts with a clean initial state, eliminating the risks associated with corrupted variables or accumulated side effects.
- Compatibility with
stateless
architectures Ideal for HTTP servers, CLI tools or ephemeral scripts where the external state (database, files) takes precedence.
Total isolation
Each reload relies on Dart isolates, independent units of execution:
- Greater stability: a failing isolate does not affect the main process.
- No dependency on the Dart VM: The code runs identically in JIT, AOT or as a native executable.
- Optimum memory management: isolates are destroyed cleanly after use, avoiding memory leaks.
Minimalism and Performance
With just 6 KB and no external dependencies, the solution stands out for :
- Virtually instantaneous reload time (<10 ms): thanks to optimised incremental compilation and lightweight isolate management.
- Seamless integration: No complex configuration is required beyond adding the package via pub add.
- Universal compatibility: Works with any Dart project, whether it's a backend, a CLI tool or a data processing script.
Backend first
Let's imagine an HTTP server (or any other application). During the development phase, you have to implement features (services, controllers, routes, etc.) for which you have to restart your application several times in order to obtain a rendering that can be tested by Postman or any other similar tool.
It is also important to note that when your application is restarted, it loses all its states in order to re-establish an initial state. This process is in line with the notion of statelessness
found in cloud natives.
The time saved is significant, while the development experience is impeccable thanks to a turnkey approach that offers a preset that can be used without any specific configuration.
However, it is important to note that if the default behaviour of the HMR when used in a ‘global’ way is not appropriate, you can design the configuration of your HMR yourself by defining your own behaviour.
Installation
The hmr
package can be used in two different ways:
- Installable CLI command
- As a dependency
Command line
In the first case, you will need to install the executable using the following command :
$ dart pub global activate hmr
All that's left is to run the command in your terminal
$ cd /path/to/your/project
$ hmr
Although ready to use, you can also alter the basic configuration by adding specifications to the pubspec.yaml
file
hmr:
# Change the location of the input file
entrypoint: bin/main.dart
# Delay before recompilation in milliseconds
# If not specified, the recompilation
# will be immediate and you don't have any debounce
debounce: 5
# Only include files that meet the following criteria
includes:
- '**/*.txt'
- '**/*.dart'
# Exclude files meeting the following criteria
excludes:
- '.dart_tool/**'
Build your own
You can create your own hmr
by exploiting the two main components of the package Watcher
and Runner
.
-
Watcher
: manages file monitoring and all the business rules applied before events are sent (create, update, delete). -
Runner
: manages isolates and project compilation.
import 'package:hmr/hmr.dart';
void main() {
final runner = Runner(
tempDirectory: Directory.systemTemp,
entrypoint: File(
path.join([
Directory.current.path,
'bin',
'main.dart'
])
));
final watcher = Watcher(
onStart: () => print('Watching for changes...'),
middlewares: [
IgnoreMiddleware(['~', '.dart_tool', '.git', '.idea', '.vscode']),
IncludeMiddleware([Glob("**.dart")]),
DebounceMiddleware(Duration(milliseconds: 5), dateTime),
],
onFileChange: (int eventType, File file) async {
final action = switch (eventType) {
FileSystemEvent.create => 'created',
FileSystemEvent.modify => 'modified',
FileSystemEvent.delete => 'deleted',
FileSystemEvent.move => 'moved',
_ => 'changed'
};
print('File $action ${file.path}');
await runner.reload();
});
watcher.watch();
runner.run();
}
How it works
From change to reload
It all starts with a change to the source code by the developer. This action triggers a cascade of automated steps:
The Watcher
, constantly monitoring the file system, instantly detects the change (creation, modification or deletion).
This detection is a non-blocking event that allows the watcher to continue monitoring other files while the reload process is underway.
Example:
- A fix is made to a calculation function.
- The Watcher identifies the file backup and transmits the event to the Runner in less than 2 ms.
Incremental compilation
Once notified, the Runner
orchestrates the targeted compilation:
The modified code is compiled into a .dill
file, Dart's intermediate binary format, which will be deposited in a temporary folder so as not to pollute the source project.
Only the modules affected by the change are recompiled, a process optimized to take less than 5 ms even on large projects.
Isolate Management
The heart of the approach lies in Dart isolates, lightweight, isolated execution environments:
- The previous instance is killed immediately, freeing up memory and preventing leaks.
- The new code (via .dill) is loaded into a blank context, guaranteeing a predictable initial state.
If the new code contains a critical error (e.g. infinite loop), only the isolate is affected. The main process remains stable, allowing subsequent reloading.
Continuous loop
The cycle repeats itself with each modification, creating an immediate feedback loop:
- The developer saves the file.
- The Watcher detects the change.
- The Runner recompiles and relaunches the isolate.
- the result is visible without any noticeable delay.
This loop is non-blocking, so that when an isolate is active, we can trigger a new reload if necessary.
Key architectural benefits
Risk Isolation
Isolates function like sandboxes, preventing propagation errors.
No shared dependencies: each instance is autonomous.
Universal compatibility
The .dill format is compatible with all Dart compilation modes (JIT, AOT, exe).
No modifications are required for production deployment.
This diagram illustrates the elegant simplicity of your implementation:
Watcher
→ Responsive vigil.
Runner
→ Efficient conductor.
Isolates
→ Disposable and reliable workers.
By combining these elements, you get a reloading system that's fast, stable and tailored to the demands of modern Dart applications, while avoiding the complexity of traditional solutions.
"A mechanic doesn't have to be complicated to be powerful."