1
1
import type { TurbopackMessageAction } from '../../../../server/dev/hot-reloader-types'
2
2
import type { Update as TurbopackUpdate } from '../../../../build/swc/types'
3
3
4
+ declare global {
5
+ interface Window {
6
+ __NEXT_HMR_TURBOPACK_REPORT_NOISY_NOOP_EVENTS : boolean | undefined
7
+ }
8
+ }
9
+
10
+ // How long to wait before reporting the HMR start, used to suppress irrelevant
11
+ // `BUILDING` events. Does not impact reported latency.
12
+ const TURBOPACK_HMR_START_DELAY_MS = 100
13
+
4
14
interface HmrUpdate {
15
+ hasUpdates : boolean
5
16
updatedModules : Set < string >
6
17
startMsSinceEpoch : number
7
18
endMsSinceEpoch : number
@@ -11,36 +22,97 @@ export class TurbopackHmr {
11
22
#updatedModules: Set < string >
12
23
#startMsSinceEpoch: number | undefined
13
24
#lastUpdateMsSinceEpoch: number | undefined
25
+ #deferredReportHmrStartId: ReturnType < typeof setTimeout > | undefined
14
26
15
27
constructor ( ) {
16
28
this . #updatedModules = new Set ( )
17
29
}
18
30
31
+ // HACK: Turbopack tends to generate a lot of irrelevant "BUILDING" actions,
32
+ // as it reports *any* compilation, including fully no-op/cached compilations
33
+ // and those unrelated to HMR. Fixing this would require significant
34
+ // architectural changes.
35
+ //
36
+ // Work around this by deferring any "rebuilding" message by 100ms. If we get
37
+ // a BUILT event within that threshold and nothing has changed, just suppress
38
+ // the message entirely.
39
+ #runDeferredReportHmrStart( ) {
40
+ if ( this . #deferredReportHmrStartId != null ) {
41
+ console . log ( '[Fast Refresh] rebuilding' )
42
+ this . #cancelDeferredReportHmrStart( )
43
+ }
44
+ }
45
+
46
+ #cancelDeferredReportHmrStart( ) {
47
+ clearTimeout ( this . #deferredReportHmrStartId)
48
+ this . #deferredReportHmrStartId = undefined
49
+ }
50
+
19
51
onBuilding ( ) {
20
52
this . #lastUpdateMsSinceEpoch = undefined
53
+ this . #cancelDeferredReportHmrStart( )
21
54
this . #startMsSinceEpoch = Date . now ( )
55
+
56
+ // report the HMR start after a short delay
57
+ this . #deferredReportHmrStartId = setTimeout (
58
+ ( ) => this . #runDeferredReportHmrStart( ) ,
59
+ // debugging feature: don't defer/suppress noisy no-op HMR update messages
60
+ self . __NEXT_HMR_TURBOPACK_REPORT_NOISY_NOOP_EVENTS
61
+ ? 0
62
+ : TURBOPACK_HMR_START_DELAY_MS
63
+ )
22
64
}
23
65
24
- onTurbopackMessage ( msg : TurbopackMessageAction ) {
66
+ /** Helper for other `onEvent` methods. */
67
+ #onUpdate( ) {
68
+ this . #runDeferredReportHmrStart( )
25
69
this . #lastUpdateMsSinceEpoch = Date . now ( )
70
+ }
71
+
72
+ onTurbopackMessage ( msg : TurbopackMessageAction ) {
73
+ this . #onUpdate( )
26
74
const updatedModules = extractModulesFromTurbopackMessage ( msg . data )
27
75
for ( const module of updatedModules ) {
28
76
this . #updatedModules. add ( module )
29
77
}
30
78
}
31
79
80
+ onServerComponentChanges ( ) {
81
+ this . #onUpdate( )
82
+ }
83
+
84
+ onReloadPage ( ) {
85
+ this . #onUpdate( )
86
+ }
87
+
88
+ onPageAddRemove ( ) {
89
+ this . #onUpdate( )
90
+ }
91
+
92
+ /**
93
+ * @returns `null` if the caller should ignore the update entirely. Returns an
94
+ * object with `hasUpdates: false` if the caller should report the end of
95
+ * the HMR in the browser console, but the HMR was a no-op.
96
+ */
32
97
onBuilt ( ) : HmrUpdate | null {
33
- // it's possible for `this.#startMsSinceEpoch` to not be set if this was the initial
34
- // computation, just return null in this case.
35
- if ( this . #startMsSinceEpoch == null ) {
98
+ // Check that we got *any* `TurbopackMessageAction`, even if
99
+ // `updatedModules` is empty (not everything gets recorded there).
100
+ //
101
+ // There's also a case where `onBuilt` gets called before `onBuilding`,
102
+ // which can happen during initial page load. Ignore that too!
103
+ const hasUpdates =
104
+ this . #lastUpdateMsSinceEpoch != null && this . #startMsSinceEpoch != null
105
+ if ( ! hasUpdates && this . #deferredReportHmrStartId != null ) {
106
+ // suppress the update entirely
107
+ this . #cancelDeferredReportHmrStart( )
36
108
return null
37
109
}
110
+ this . #runDeferredReportHmrStart( )
111
+
38
112
const result = {
113
+ hasUpdates,
39
114
updatedModules : this . #updatedModules,
40
115
startMsSinceEpoch : this . #startMsSinceEpoch! ,
41
- // Turbopack has a debounce which causes every BUILT message to appear
42
- // 30ms late. We don't want to include this latency in our reporting, so
43
- // prefer to use the last TURBOPACK_MESSAGE time.
44
116
endMsSinceEpoch : this . #lastUpdateMsSinceEpoch ?? Date . now ( ) ,
45
117
}
46
118
this . #updatedModules = new Set ( )
0 commit comments