react-tooltip 6.0.0-beta.1179.rc.2 → 6.0.0-beta.1179.rc.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +26 -0
- package/benchmarks/aggregate-benchmarks.mjs +258 -0
- package/benchmarks/fixture/app.tsx +278 -0
- package/benchmarks/fixture/index.html +54 -0
- package/benchmarks/run-benchmark.mjs +324 -0
- package/benchmarks/run-scaling-series.mjs +77 -0
- package/dist/react-tooltip-tokens.css +1 -0
- package/dist/react-tooltip.cjs +780 -613
- package/dist/react-tooltip.cjs.map +1 -1
- package/dist/react-tooltip.css +17 -3
- package/dist/react-tooltip.d.ts +14 -21
- package/dist/react-tooltip.min.cjs +2 -2
- package/dist/react-tooltip.min.cjs.map +1 -1
- package/dist/react-tooltip.min.css +1 -1
- package/dist/react-tooltip.min.mjs +2 -2
- package/dist/react-tooltip.min.mjs.map +1 -1
- package/dist/react-tooltip.mjs +781 -614
- package/dist/react-tooltip.mjs.map +1 -1
- package/dist/react-tooltip.umd.js +783 -616
- package/dist/react-tooltip.umd.js.map +1 -1
- package/dist/react-tooltip.umd.min.js +2 -2
- package/dist/react-tooltip.umd.min.js.map +1 -1
- package/global.d.ts +7 -0
- package/package.json +41 -30
- package/rollup.config.dev.mjs +0 -1
- package/rollup.config.prod.mjs +15 -5
- package/rollup.config.types.mjs +1 -5
- package/scripts/configure-react-version.mjs +52 -0
- package/tsconfig.jest.json +14 -0
- package/.eslintrc.json +0 -97
- package/.gitattributes +0 -3
- package/.prettierrc.json +0 -10
- package/.stylelintrc.json +0 -19
- package/beta-release.js +0 -81
- package/tsconfig.json +0 -109
package/README.md
CHANGED
|
@@ -42,6 +42,32 @@ or
|
|
|
42
42
|
yarn add react-tooltip
|
|
43
43
|
```
|
|
44
44
|
|
|
45
|
+
## React Compatibility
|
|
46
|
+
|
|
47
|
+
`react-tooltip` supports React `16.14.0` and newer through peer dependencies, including React 17, 18, and 19.
|
|
48
|
+
|
|
49
|
+
The project is currently validated against React 19, but the published package remains compatible with older supported React versions.
|
|
50
|
+
|
|
51
|
+
| React version | Supported |
|
|
52
|
+
| ------------- | --------- |
|
|
53
|
+
| 16.14+ | Yes |
|
|
54
|
+
| 17.x | Yes |
|
|
55
|
+
| 18.x | Yes |
|
|
56
|
+
| 19.x | Yes |
|
|
57
|
+
|
|
58
|
+
## Server Components
|
|
59
|
+
|
|
60
|
+
`react-tooltip` is a client-side library. It uses hooks, DOM observers, browser events, and layout measurement, so the tooltip component itself must run inside a client component boundary.
|
|
61
|
+
|
|
62
|
+
This works well in frameworks such as Next.js with Server Components, but you should render `<Tooltip />` from a client component and attach your tooltip attributes or selectors from elements rendered under that client boundary.
|
|
63
|
+
|
|
64
|
+
If you are using React Server Components, the practical rule is simple:
|
|
65
|
+
|
|
66
|
+
- server components can render the anchor markup
|
|
67
|
+
- client components should render and control `react-tooltip`
|
|
68
|
+
|
|
69
|
+
In Next.js, the usual pattern is to export the tooltip from a small wrapper file marked with `'use client'`.
|
|
70
|
+
|
|
45
71
|
## Sponsors
|
|
46
72
|
|
|
47
73
|
### Gold Sponsors 🌟
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
import { readdir, readFile, writeFile } from 'fs/promises'
|
|
2
|
+
import path from 'path'
|
|
3
|
+
import { fileURLToPath } from 'url'
|
|
4
|
+
import minimist from 'minimist'
|
|
5
|
+
|
|
6
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
|
7
|
+
const resultsDir = path.join(__dirname, 'results')
|
|
8
|
+
|
|
9
|
+
const args = minimist(process.argv.slice(2))
|
|
10
|
+
const latest = Number(args.latest ?? 3)
|
|
11
|
+
const useAll = Boolean(args.all)
|
|
12
|
+
const generationArg = args.generation
|
|
13
|
+
const useAllGenerations = Boolean(args['all-generations'])
|
|
14
|
+
const explicitFiles = args._.map((inputPath) => path.resolve(inputPath))
|
|
15
|
+
|
|
16
|
+
function aggregateNumbers(values) {
|
|
17
|
+
const sorted = values
|
|
18
|
+
.filter((value) => typeof value === 'number' && Number.isFinite(value))
|
|
19
|
+
.sort((left, right) => left - right)
|
|
20
|
+
|
|
21
|
+
if (sorted.length === 0) {
|
|
22
|
+
return {
|
|
23
|
+
median: null,
|
|
24
|
+
p95: null,
|
|
25
|
+
min: null,
|
|
26
|
+
max: null,
|
|
27
|
+
mean: null,
|
|
28
|
+
standardDeviation: null,
|
|
29
|
+
spreadPercent: null,
|
|
30
|
+
sampleCount: 0,
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const middle = Math.floor(sorted.length / 2)
|
|
35
|
+
const median =
|
|
36
|
+
sorted.length % 2 === 0
|
|
37
|
+
? (sorted[middle - 1] + sorted[middle]) / 2
|
|
38
|
+
: sorted[middle]
|
|
39
|
+
const mean = sorted.reduce((total, value) => total + value, 0) / sorted.length
|
|
40
|
+
const variance =
|
|
41
|
+
sorted.reduce((total, value) => total + (value - mean) ** 2, 0) / sorted.length
|
|
42
|
+
const standardDeviation = Math.sqrt(variance)
|
|
43
|
+
const p95 = sorted[Math.min(sorted.length - 1, Math.ceil(sorted.length * 0.95) - 1)]
|
|
44
|
+
|
|
45
|
+
return {
|
|
46
|
+
median,
|
|
47
|
+
p95,
|
|
48
|
+
min: sorted[0],
|
|
49
|
+
max: sorted[sorted.length - 1],
|
|
50
|
+
mean,
|
|
51
|
+
standardDeviation,
|
|
52
|
+
spreadPercent: median === 0 ? null : ((p95 - median) / Math.abs(median)) * 100,
|
|
53
|
+
sampleCount: sorted.length,
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function formatMs(value) {
|
|
58
|
+
return typeof value === 'number' ? `${value.toFixed(2)} ms` : '—'
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function formatBytes(value) {
|
|
62
|
+
return typeof value === 'number' ? `${(value / 1024).toFixed(1)} KiB` : '—'
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function formatPercent(value) {
|
|
66
|
+
return typeof value === 'number' ? `${value.toFixed(1)}%` : '—'
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function timestampId() {
|
|
70
|
+
return new Date().toISOString().replace(/[:.]/g, '-')
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function getRunGeneration(run) {
|
|
74
|
+
return Number.isInteger(run?.benchmarkVersion) ? run.benchmarkVersion : 0
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async function resolveInputFiles() {
|
|
78
|
+
if (explicitFiles.length > 0) {
|
|
79
|
+
return explicitFiles
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const entries = await readdir(resultsDir)
|
|
83
|
+
const scalingEntries = entries
|
|
84
|
+
.filter((entry) => entry.endsWith('.json'))
|
|
85
|
+
.filter((entry) => entry.startsWith('scaling-'))
|
|
86
|
+
.map((entry) => path.join(resultsDir, entry))
|
|
87
|
+
.sort()
|
|
88
|
+
|
|
89
|
+
if (useAll) {
|
|
90
|
+
return scalingEntries
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return scalingEntries.slice(-latest)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function resolveGenerationSelection(runs) {
|
|
97
|
+
if (useAllGenerations) {
|
|
98
|
+
return {
|
|
99
|
+
selectedRuns: runs,
|
|
100
|
+
generationLabel: 'all benchmark generations',
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (generationArg !== undefined) {
|
|
105
|
+
const requestedGeneration =
|
|
106
|
+
generationArg === 'latest' ? Math.max(...runs.map(getRunGeneration)) : Number(generationArg)
|
|
107
|
+
|
|
108
|
+
if (!Number.isInteger(requestedGeneration)) {
|
|
109
|
+
throw new Error(`Invalid --generation value: ${generationArg}`)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return {
|
|
113
|
+
selectedRuns: runs.filter((run) => getRunGeneration(run) === requestedGeneration),
|
|
114
|
+
generationLabel:
|
|
115
|
+
generationArg === 'latest'
|
|
116
|
+
? `latest benchmark generation (${requestedGeneration})`
|
|
117
|
+
: `benchmark generation ${requestedGeneration}`,
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return {
|
|
122
|
+
selectedRuns: runs,
|
|
123
|
+
generationLabel: 'all benchmark generations',
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function buildMarkdownReport(result) {
|
|
128
|
+
const lines = [
|
|
129
|
+
'# Aggregated React Tooltip Scaling Benchmark',
|
|
130
|
+
'',
|
|
131
|
+
`- Timestamp: ${result.timestamp}`,
|
|
132
|
+
`- Input files: ${result.inputFiles.length}`,
|
|
133
|
+
`- Selection: ${result.selection}`,
|
|
134
|
+
`- Generation filter: ${result.generationFilter}`,
|
|
135
|
+
`- Counts: ${result.counts.join(', ')}`,
|
|
136
|
+
'',
|
|
137
|
+
'| Count | V5 mount | V6 mount | Mount delta | Mount spread | V5 unmount | V6 unmount | Unmount delta | Unmount spread | V5 mount mem | V6 mount mem | Mount mem delta | Mount mem spread | V5 unmount mem | V6 unmount mem | Unmount mem delta | Unmount mem spread | Samples | V5 timeouts | V6 timeouts |',
|
|
138
|
+
'| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |',
|
|
139
|
+
]
|
|
140
|
+
|
|
141
|
+
result.summary.forEach((row) => {
|
|
142
|
+
lines.push(
|
|
143
|
+
`| ${row.count} | ${formatMs(row.v5.mount.median)} | ${formatMs(row.v6.mount.median)} | ${formatMs(row.mountDeltaMs)} | ${formatPercent(row.mountDeltaSpreadPercent)} | ${formatMs(row.v5.unmount.median)} | ${formatMs(row.v6.unmount.median)} | ${formatMs(row.unmountDeltaMs)} | ${formatPercent(row.unmountDeltaSpreadPercent)} | ${formatBytes(row.v5.mountMemory.median)} | ${formatBytes(row.v6.mountMemory.median)} | ${formatBytes(row.mountMemoryDeltaBytes)} | ${formatPercent(row.mountMemoryDeltaSpreadPercent)} | ${formatBytes(row.v5.unmountMemory.median)} | ${formatBytes(row.v6.unmountMemory.median)} | ${formatBytes(row.unmountMemoryDeltaBytes)} | ${formatPercent(row.unmountMemoryDeltaSpreadPercent)} | ${row.sampleCount} | ${row.v5.timeoutCount} | ${row.v6.timeoutCount} |`,
|
|
144
|
+
)
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
return `${lines.join('\n')}\n`
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
async function main() {
|
|
151
|
+
const inputFiles = await resolveInputFiles()
|
|
152
|
+
|
|
153
|
+
if (inputFiles.length === 0) {
|
|
154
|
+
throw new Error('No benchmark result files were found to aggregate.')
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const allRuns = await Promise.all(
|
|
158
|
+
inputFiles.map(async (inputFile) => JSON.parse(await readFile(inputFile, 'utf8'))),
|
|
159
|
+
)
|
|
160
|
+
const { selectedRuns: runs, generationLabel } = resolveGenerationSelection(allRuns)
|
|
161
|
+
|
|
162
|
+
if (runs.length === 0) {
|
|
163
|
+
throw new Error('No benchmark result files matched the requested generation filter.')
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const counts = Array.from(
|
|
167
|
+
new Set(runs.flatMap((run) => run.counts ?? [])),
|
|
168
|
+
).sort((left, right) => left - right)
|
|
169
|
+
|
|
170
|
+
const summary = counts.map((count) => {
|
|
171
|
+
const rows = runs
|
|
172
|
+
.map((run) => run.summary?.find((entry) => entry.count === count))
|
|
173
|
+
.filter(Boolean)
|
|
174
|
+
|
|
175
|
+
const aggregateVersion = (version) => ({
|
|
176
|
+
mount: aggregateNumbers(rows.map((row) => row[version]?.mount?.median)),
|
|
177
|
+
unmount: aggregateNumbers(rows.map((row) => row[version]?.unmount?.median)),
|
|
178
|
+
mountMemory: aggregateNumbers(rows.map((row) => row[version]?.mountMemory?.median)),
|
|
179
|
+
unmountMemory: aggregateNumbers(rows.map((row) => row[version]?.unmountMemory?.median)),
|
|
180
|
+
timeoutCount: rows.reduce(
|
|
181
|
+
(total, row) => total + (row[version]?.timeoutCount ?? 0),
|
|
182
|
+
0,
|
|
183
|
+
),
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
const v5 = aggregateVersion('v5')
|
|
187
|
+
const v6 = aggregateVersion('v6')
|
|
188
|
+
const sampleCount = Math.max(v5.mount.sampleCount, v6.mount.sampleCount)
|
|
189
|
+
|
|
190
|
+
const mountDeltaMs =
|
|
191
|
+
typeof v5.mount.median === 'number' && typeof v6.mount.median === 'number'
|
|
192
|
+
? v6.mount.median - v5.mount.median
|
|
193
|
+
: null
|
|
194
|
+
const unmountDeltaMs =
|
|
195
|
+
typeof v5.unmount.median === 'number' && typeof v6.unmount.median === 'number'
|
|
196
|
+
? v6.unmount.median - v5.unmount.median
|
|
197
|
+
: null
|
|
198
|
+
const mountMemoryDeltaBytes =
|
|
199
|
+
typeof v5.mountMemory.median === 'number' && typeof v6.mountMemory.median === 'number'
|
|
200
|
+
? v6.mountMemory.median - v5.mountMemory.median
|
|
201
|
+
: null
|
|
202
|
+
const unmountMemoryDeltaBytes =
|
|
203
|
+
typeof v5.unmountMemory.median === 'number' && typeof v6.unmountMemory.median === 'number'
|
|
204
|
+
? v6.unmountMemory.median - v5.unmountMemory.median
|
|
205
|
+
: null
|
|
206
|
+
|
|
207
|
+
return {
|
|
208
|
+
count,
|
|
209
|
+
sampleCount,
|
|
210
|
+
v5,
|
|
211
|
+
v6,
|
|
212
|
+
mountDeltaMs,
|
|
213
|
+
unmountDeltaMs,
|
|
214
|
+
mountMemoryDeltaBytes,
|
|
215
|
+
unmountMemoryDeltaBytes,
|
|
216
|
+
mountDeltaSpreadPercent:
|
|
217
|
+
typeof v5.mount.spreadPercent === 'number' && typeof v6.mount.spreadPercent === 'number'
|
|
218
|
+
? Math.max(v5.mount.spreadPercent, v6.mount.spreadPercent)
|
|
219
|
+
: null,
|
|
220
|
+
unmountDeltaSpreadPercent:
|
|
221
|
+
typeof v5.unmount.spreadPercent === 'number' && typeof v6.unmount.spreadPercent === 'number'
|
|
222
|
+
? Math.max(v5.unmount.spreadPercent, v6.unmount.spreadPercent)
|
|
223
|
+
: null,
|
|
224
|
+
mountMemoryDeltaSpreadPercent:
|
|
225
|
+
typeof v5.mountMemory.spreadPercent === 'number' && typeof v6.mountMemory.spreadPercent === 'number'
|
|
226
|
+
? Math.max(v5.mountMemory.spreadPercent, v6.mountMemory.spreadPercent)
|
|
227
|
+
: null,
|
|
228
|
+
unmountMemoryDeltaSpreadPercent:
|
|
229
|
+
typeof v5.unmountMemory.spreadPercent === 'number' && typeof v6.unmountMemory.spreadPercent === 'number'
|
|
230
|
+
? Math.max(v5.unmountMemory.spreadPercent, v6.unmountMemory.spreadPercent)
|
|
231
|
+
: null,
|
|
232
|
+
}
|
|
233
|
+
})
|
|
234
|
+
|
|
235
|
+
const result = {
|
|
236
|
+
id: `aggregate-scaling-${timestampId()}`,
|
|
237
|
+
timestamp: new Date().toISOString(),
|
|
238
|
+
selection: useAll ? 'all scaling runs' : `latest ${latest} scaling run(s)`,
|
|
239
|
+
generationFilter: generationLabel,
|
|
240
|
+
inputFiles: runs.map((run, index) => inputFiles[allRuns.indexOf(run)] ?? inputFiles[index]),
|
|
241
|
+
counts,
|
|
242
|
+
summary,
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const jsonPath = path.join(resultsDir, `${result.id}.json`)
|
|
246
|
+
const markdownPath = path.join(resultsDir, `${result.id}.md`)
|
|
247
|
+
|
|
248
|
+
await writeFile(jsonPath, `${JSON.stringify(result, null, 2)}\n`, 'utf8')
|
|
249
|
+
await writeFile(markdownPath, buildMarkdownReport(result), 'utf8')
|
|
250
|
+
|
|
251
|
+
console.log(`Saved aggregate benchmark results to ${jsonPath}`)
|
|
252
|
+
console.log(`Saved aggregate benchmark report to ${markdownPath}`)
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
main().catch((error) => {
|
|
256
|
+
console.error(error)
|
|
257
|
+
process.exitCode = 1
|
|
258
|
+
})
|
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
import React, { useEffect, useMemo, useState } from 'react'
|
|
2
|
+
import { flushSync } from 'react-dom'
|
|
3
|
+
import { createRoot } from 'react-dom/client'
|
|
4
|
+
import { Tooltip as TooltipV5 } from '../../docs/node_modules/react-tooltip/dist/react-tooltip.mjs'
|
|
5
|
+
import { Tooltip as TooltipV6 } from '../../dist/react-tooltip.esm.js'
|
|
6
|
+
|
|
7
|
+
type BenchmarkVersion = 'v5' | 'v6'
|
|
8
|
+
|
|
9
|
+
type RenderMode = 'shared'
|
|
10
|
+
|
|
11
|
+
type FixtureState = {
|
|
12
|
+
version: BenchmarkVersion
|
|
13
|
+
count: number
|
|
14
|
+
renderMode: RenderMode
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
type ScenarioSample = {
|
|
18
|
+
count: number
|
|
19
|
+
mountDurationMs: number | null
|
|
20
|
+
unmountDurationMs: number | null
|
|
21
|
+
mountMemoryDeltaBytes: number | null
|
|
22
|
+
unmountMemoryDeltaBytes: number | null
|
|
23
|
+
timedOut: boolean
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
type ScenarioResult = {
|
|
27
|
+
version: BenchmarkVersion
|
|
28
|
+
renderMode: RenderMode
|
|
29
|
+
samplesByCount: ScenarioSample[]
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
type BenchmarkHarness = {
|
|
33
|
+
runScalingBenchmark: (args: {
|
|
34
|
+
version: BenchmarkVersion
|
|
35
|
+
counts: number[]
|
|
36
|
+
timeoutMs: number
|
|
37
|
+
repeats: number
|
|
38
|
+
warmups: number
|
|
39
|
+
renderMode: RenderMode
|
|
40
|
+
onProgress?: (message: string) => void
|
|
41
|
+
}) => Promise<ScenarioResult>
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
declare global {
|
|
45
|
+
interface Window {
|
|
46
|
+
__reactTooltipBenchmark?: BenchmarkHarness
|
|
47
|
+
__REACT_DEVTOOLS_GLOBAL_HOOK__?: {
|
|
48
|
+
isDisabled?: boolean
|
|
49
|
+
}
|
|
50
|
+
gc?: () => void
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
window.__REACT_DEVTOOLS_GLOBAL_HOOK__ = {
|
|
55
|
+
isDisabled: true,
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function readUsedHeapBytes() {
|
|
59
|
+
const performanceWithMemory = window.performance as Performance & {
|
|
60
|
+
memory?: {
|
|
61
|
+
usedJSHeapSize: number
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return performanceWithMemory.memory?.usedJSHeapSize ?? null
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function nextFrame() {
|
|
69
|
+
return new Promise<void>((resolve) => {
|
|
70
|
+
window.requestAnimationFrame(() => resolve())
|
|
71
|
+
})
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async function collectGarbage() {
|
|
75
|
+
if (typeof window.gc === 'function') {
|
|
76
|
+
window.gc()
|
|
77
|
+
await nextFrame()
|
|
78
|
+
window.gc()
|
|
79
|
+
await nextFrame()
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async function readStableHeapBytes() {
|
|
84
|
+
await collectGarbage()
|
|
85
|
+
return readUsedHeapBytes()
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async function waitUntil(predicate: () => boolean, timeoutMs: number) {
|
|
89
|
+
const startedAt = window.performance.now()
|
|
90
|
+
|
|
91
|
+
while (window.performance.now() - startedAt < timeoutMs) {
|
|
92
|
+
if (predicate()) {
|
|
93
|
+
return true
|
|
94
|
+
}
|
|
95
|
+
await nextFrame()
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return false
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function BenchmarkFixture({ version, count }: FixtureState) {
|
|
102
|
+
const TooltipComponent = version === 'v5' ? TooltipV5 : TooltipV6
|
|
103
|
+
const tooltipId = `benchmark-tooltip-${version}`
|
|
104
|
+
|
|
105
|
+
const anchorIds = useMemo(
|
|
106
|
+
() => Array.from({ length: count }, (_, index) => `anchor-${version}-${index}`),
|
|
107
|
+
[count, version],
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
useEffect(() => {
|
|
111
|
+
document.body.setAttribute('data-benchmark-count', String(count))
|
|
112
|
+
document.body.setAttribute('data-benchmark-version', version)
|
|
113
|
+
}, [count, version])
|
|
114
|
+
|
|
115
|
+
return (
|
|
116
|
+
<div className="benchmark-surface" aria-hidden="true">
|
|
117
|
+
<div className="anchor-grid">
|
|
118
|
+
{anchorIds.map((id, index) => (
|
|
119
|
+
<button
|
|
120
|
+
key={id}
|
|
121
|
+
id={id}
|
|
122
|
+
className="anchor"
|
|
123
|
+
data-tooltip-id={tooltipId}
|
|
124
|
+
data-tooltip-content={`Anchor ${index}`}
|
|
125
|
+
type="button"
|
|
126
|
+
>
|
|
127
|
+
{index}
|
|
128
|
+
</button>
|
|
129
|
+
))}
|
|
130
|
+
</div>
|
|
131
|
+
<TooltipComponent id={tooltipId} />
|
|
132
|
+
</div>
|
|
133
|
+
)
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const rootNode = document.getElementById('root')
|
|
137
|
+
|
|
138
|
+
if (!rootNode) {
|
|
139
|
+
throw new Error('Benchmark root element was not found.')
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const benchmarkRoot = createRoot(rootNode)
|
|
143
|
+
|
|
144
|
+
function renderFixture(nextState: FixtureState) {
|
|
145
|
+
return new Promise<void>((resolve) => {
|
|
146
|
+
flushSync(() => {
|
|
147
|
+
benchmarkRoot.render(<BenchmarkFixture {...nextState} />)
|
|
148
|
+
})
|
|
149
|
+
resolve()
|
|
150
|
+
})
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function unmountFixture() {
|
|
154
|
+
return new Promise<void>((resolve) => {
|
|
155
|
+
flushSync(() => {
|
|
156
|
+
benchmarkRoot.render(<></>)
|
|
157
|
+
})
|
|
158
|
+
resolve()
|
|
159
|
+
})
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
async function runScalingBenchmark({
|
|
163
|
+
version,
|
|
164
|
+
counts,
|
|
165
|
+
timeoutMs,
|
|
166
|
+
repeats,
|
|
167
|
+
warmups,
|
|
168
|
+
renderMode,
|
|
169
|
+
onProgress,
|
|
170
|
+
}: {
|
|
171
|
+
version: BenchmarkVersion
|
|
172
|
+
counts: number[]
|
|
173
|
+
timeoutMs: number
|
|
174
|
+
repeats: number
|
|
175
|
+
warmups: number
|
|
176
|
+
renderMode: RenderMode
|
|
177
|
+
onProgress?: (message: string) => void
|
|
178
|
+
}): Promise<ScenarioResult> {
|
|
179
|
+
const samplesByCount: ScenarioSample[] = []
|
|
180
|
+
|
|
181
|
+
for (const count of counts) {
|
|
182
|
+
for (let warmupIndex = 0; warmupIndex < warmups; warmupIndex += 1) {
|
|
183
|
+
onProgress?.(`count=${count} warmup ${warmupIndex + 1}/${warmups}`)
|
|
184
|
+
await renderFixture({
|
|
185
|
+
version,
|
|
186
|
+
count: 0,
|
|
187
|
+
renderMode,
|
|
188
|
+
})
|
|
189
|
+
await nextFrame()
|
|
190
|
+
await renderFixture({
|
|
191
|
+
version,
|
|
192
|
+
count,
|
|
193
|
+
renderMode,
|
|
194
|
+
})
|
|
195
|
+
await waitUntil(
|
|
196
|
+
() => document.querySelectorAll('[data-tooltip-id]').length === count,
|
|
197
|
+
timeoutMs,
|
|
198
|
+
)
|
|
199
|
+
await nextFrame()
|
|
200
|
+
await unmountFixture()
|
|
201
|
+
await nextFrame()
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
for (let repeatIndex = 0; repeatIndex < repeats; repeatIndex += 1) {
|
|
205
|
+
onProgress?.(`count=${count} repeat ${repeatIndex + 1}/${repeats}`)
|
|
206
|
+
await renderFixture({
|
|
207
|
+
version,
|
|
208
|
+
count: 0,
|
|
209
|
+
renderMode,
|
|
210
|
+
})
|
|
211
|
+
await nextFrame()
|
|
212
|
+
|
|
213
|
+
const mountMemoryBefore = await readStableHeapBytes()
|
|
214
|
+
const mountStartedAt = window.performance.now()
|
|
215
|
+
|
|
216
|
+
await renderFixture({
|
|
217
|
+
version,
|
|
218
|
+
count,
|
|
219
|
+
renderMode,
|
|
220
|
+
})
|
|
221
|
+
|
|
222
|
+
const mountReady = await waitUntil(() => {
|
|
223
|
+
return document.querySelectorAll('[data-tooltip-id]').length === count
|
|
224
|
+
}, timeoutMs)
|
|
225
|
+
|
|
226
|
+
await nextFrame()
|
|
227
|
+
|
|
228
|
+
const mountEndedAt = window.performance.now()
|
|
229
|
+
|
|
230
|
+
const mountMemoryAfter = await readStableHeapBytes()
|
|
231
|
+
const unmountMemoryBefore = mountMemoryAfter
|
|
232
|
+
const unmountStartedAt = window.performance.now()
|
|
233
|
+
|
|
234
|
+
await unmountFixture()
|
|
235
|
+
|
|
236
|
+
const unmountReady = await waitUntil(() => {
|
|
237
|
+
return document.querySelectorAll('[data-tooltip-id]').length === 0
|
|
238
|
+
}, timeoutMs)
|
|
239
|
+
|
|
240
|
+
await nextFrame()
|
|
241
|
+
|
|
242
|
+
const unmountEndedAt = window.performance.now()
|
|
243
|
+
|
|
244
|
+
const unmountMemoryAfter = await readStableHeapBytes()
|
|
245
|
+
|
|
246
|
+
samplesByCount.push({
|
|
247
|
+
count,
|
|
248
|
+
mountDurationMs: mountReady ? mountEndedAt - mountStartedAt : null,
|
|
249
|
+
unmountDurationMs: unmountReady ? unmountEndedAt - unmountStartedAt : null,
|
|
250
|
+
mountMemoryDeltaBytes:
|
|
251
|
+
mountReady && mountMemoryBefore !== null && mountMemoryAfter !== null
|
|
252
|
+
? mountMemoryAfter - mountMemoryBefore
|
|
253
|
+
: null,
|
|
254
|
+
unmountMemoryDeltaBytes:
|
|
255
|
+
unmountReady && unmountMemoryBefore !== null && unmountMemoryAfter !== null
|
|
256
|
+
? unmountMemoryAfter - unmountMemoryBefore
|
|
257
|
+
: null,
|
|
258
|
+
timedOut: !mountReady || !unmountReady,
|
|
259
|
+
})
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
return {
|
|
264
|
+
version,
|
|
265
|
+
renderMode,
|
|
266
|
+
samplesByCount,
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
window.__reactTooltipBenchmark = {
|
|
271
|
+
runScalingBenchmark,
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
renderFixture({
|
|
275
|
+
version: 'v6',
|
|
276
|
+
count: 0,
|
|
277
|
+
renderMode: 'shared',
|
|
278
|
+
})
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
6
|
+
<title>React Tooltip Benchmark</title>
|
|
7
|
+
<style>
|
|
8
|
+
html,
|
|
9
|
+
body {
|
|
10
|
+
margin: 0;
|
|
11
|
+
padding: 0;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
body {
|
|
15
|
+
font-family: sans-serif;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
#root {
|
|
19
|
+
min-height: 100vh;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
.benchmark-surface {
|
|
23
|
+
left: 24px;
|
|
24
|
+
opacity: 0.02;
|
|
25
|
+
pointer-events: none;
|
|
26
|
+
position: fixed;
|
|
27
|
+
top: 24px;
|
|
28
|
+
width: 1200px;
|
|
29
|
+
z-index: -1;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
.anchor-grid {
|
|
33
|
+
display: grid;
|
|
34
|
+
gap: 8px;
|
|
35
|
+
grid-template-columns: repeat(10, minmax(0, 1fr));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
.anchor {
|
|
39
|
+
align-items: center;
|
|
40
|
+
background: #111827;
|
|
41
|
+
border: 0;
|
|
42
|
+
border-radius: 6px;
|
|
43
|
+
color: #fff;
|
|
44
|
+
display: inline-flex;
|
|
45
|
+
height: 28px;
|
|
46
|
+
justify-content: center;
|
|
47
|
+
}
|
|
48
|
+
</style>
|
|
49
|
+
</head>
|
|
50
|
+
<body>
|
|
51
|
+
<div id="root"></div>
|
|
52
|
+
<script type="module" src="/app.js"></script>
|
|
53
|
+
</body>
|
|
54
|
+
</html>
|