diff options
author | 2021-11-21 03:52:14 -0800 | |
---|---|---|
committer | 2021-11-21 03:52:14 -0800 | |
commit | 469a36e3b6c7cffaceeddad6b57e51c55bf2493d (patch) | |
tree | df6f7afa97c63f9c1bf28c5b431b7d1211b024fb | |
parent | f9d333bec21a43f02c6dd8d98e402ab4e925c72a (diff) | |
download | bun-469a36e3b6c7cffaceeddad6b57e51c55bf2493d.tar.gz bun-469a36e3b6c7cffaceeddad6b57e51c55bf2493d.tar.zst bun-469a36e3b6c7cffaceeddad6b57e51c55bf2493d.zip |
[HMR] Large perf improvement for JS hot reloads at runtime
From benchmarking, I noticed that a lot of time was spent running
`HMRModule.update()`.
We don't need to call that function if updates for the same module ID have not changed any exports
-rw-r--r-- | src/runtime.version | 2 | ||||
-rw-r--r-- | src/runtime/hmr.ts | 56 |
2 files changed, 50 insertions, 8 deletions
diff --git a/src/runtime.version b/src/runtime.version index 736b96d9d..66b29714a 100644 --- a/src/runtime.version +++ b/src/runtime.version @@ -1 +1 @@ -f41519acbec321a3
\ No newline at end of file +4decc1484daa0fb4
\ No newline at end of file diff --git a/src/runtime/hmr.ts b/src/runtime/hmr.ts index 77873a34c..3803dff2e 100644 --- a/src/runtime/hmr.ts +++ b/src/runtime/hmr.ts @@ -1118,19 +1118,36 @@ if (typeof window !== "undefined") { } const callbacksStart = performance.now(); - const origUpdaters = - HMRModule.dependencies.modules[ - this.module_index - ].additional_updaters.slice(); + const origUpdaters = oldModule ? oldModule.additional_updaters.slice() : []; try { switch (this.reloader) { case ReloadBehavior.hotReload: { let foundBoundary = false; + const isOldModuleDead = oldModule && oldModule.previousVersion && oldModule.previousVersion.id === oldModule.id && oldModule.hasSameExports(oldModule.previousVersion); + if (oldModule) { - HMRModule.dependencies.modules[ - this.module_index - ].additional_updaters.push(oldModule.update.bind(oldModule)); + // ESM-based HMR has a disadvantage against CommonJS HMR + // ES Namespace objects are not [[Configurable]] + // That means we have to loop through all previous versions of updated modules that that have unique export names + // and updates those exports specifically + // Otherwise, changes will not be reflected properly + // However, we only need to loop through modules that add or remove exports, i.e. those are ones which have "real" exports + if (!isOldModuleDead) { + HMRModule.dependencies.modules[ + this.module_index + ].additional_updaters.push(oldModule.update.bind(oldModule)); + HMRModule.dependencies.modules[ + this.module_index + ].previousVersion = oldModule; + } else { + HMRModule.dependencies.modules[ + this.module_index + ].previousVersion = oldModule.previousVersion; + HMRModule.dependencies.modules[ + this.module_index + ].additional_updaters = origUpdaters; + } } const end = Math.min( @@ -1190,8 +1207,11 @@ if (typeof window !== "undefined") { foundBoundary ) { FastRefreshLoader.RefreshRuntime.performReactRefresh(); + // Remove potential memory leak + if (isOldModuleDead) oldModule.previousVersion = null; } else if (pendingUpdateCount === currentPendingUpdateCount) { FastRefreshLoader.performFullRefresh(); + } else { return Promise.reject( new ThrottleModuleUpdateError( @@ -1199,6 +1219,8 @@ if (typeof window !== "undefined") { ) ); } + + break; } } @@ -1292,6 +1314,25 @@ if (typeof window !== "undefined") { HMRModule.dependencies.graph[this.graph_index] = this.id; } + previousVersion = null; + + hasSameExports(that: AnyHMRModule) { + const thisKeys = Object.keys(this.exports); + const thatKeys = Object.keys(that.exports); + if (thisKeys.length !== thatKeys.length) { + return false; + } + + for (let i = 0; i < thisKeys.length; i++) { + if (thisKeys[i] !== thatKeys[i]) { + return false; + } + } + + return true; + } + + additional_files = []; additional_updaters = []; _update: (exports: Object) => void; @@ -1370,6 +1411,7 @@ if (typeof window !== "undefined") { this.$r_(Component, Component.name || Component.displayName); } + // Auto-register exported React components so we only have to manually register the non-exported ones // This is what Metro does: https://github.com/facebook/metro/blob/9f2b1210a0f66378dd93e5fcaabc464c86c9e236/packages/metro-runtime/src/polyfills/require.js#L905 exportAll(object: any) { |