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,17 +22,51 @@ 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
+ if ( self . __NEXT_HMR_TURBOPACK_REPORT_NOISY_NOOP_EVENTS ) {
57
+ // debugging feature: don't defer/suppress noisy no-op HMR update messages
58
+ this . #runDeferredReportHmrStart( )
59
+ } else {
60
+ // report the HMR start after a short delay
61
+ this . #deferredReportHmrStartId = setTimeout (
62
+ ( ) => this . #runDeferredReportHmrStart( ) ,
63
+ TURBOPACK_HMR_START_DELAY_MS
64
+ )
65
+ }
22
66
}
23
67
24
68
onTurbopackMessage ( msg : TurbopackMessageAction ) {
69
+ this . #runDeferredReportHmrStart( )
25
70
this . #lastUpdateMsSinceEpoch = Date . now ( )
26
71
const updatedModules = extractModulesFromTurbopackMessage ( msg . data )
27
72
for ( const module of updatedModules ) {
@@ -30,17 +75,23 @@ export class TurbopackHmr {
30
75
}
31
76
32
77
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 ) {
78
+ // Check that we got *any* `TurbopackMessageAction`, even if `updatedModules` is empty (not
79
+ // everything gets recorded there).
80
+ //
81
+ // This also handles for the case where `onBuilt` gets called before `onBuilding`, which can
82
+ // happen during initial page load. We should ignore this.
83
+ const hasUpdates = this . #lastUpdateMsSinceEpoch != null
84
+ if ( ! hasUpdates && this . #deferredReportHmrStartId != null ) {
85
+ // suppress the update entirely
86
+ this . #cancelDeferredReportHmrStart( )
36
87
return null
37
88
}
89
+ this . #runDeferredReportHmrStart( )
90
+
38
91
const result = {
92
+ hasUpdates,
39
93
updatedModules : this . #updatedModules,
40
94
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
95
endMsSinceEpoch : this . #lastUpdateMsSinceEpoch ?? Date . now ( ) ,
45
96
}
46
97
this . #updatedModules = new Set ( )
0 commit comments