bye-thrash 0.0.1
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 +205 -0
- package/dist/index.d.mts +45 -0
- package/dist/index.d.ts +45 -0
- package/dist/index.js +319 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +315 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +51 -0
package/README.md
ADDED
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
# <img src="./static/JOYCO.png" alt="JOYCO Logo" height="36" width="36" align="top" /> JOYCO | Bye Thrash
|
|
2
|
+
|
|
3
|
+
### Detect and destroy layout thrashing
|
|
4
|
+
|
|
5
|
+
A dev-only library that patches browser APIs to detect layout-triggering reads after style mutations in the same animation frame.
|
|
6
|
+
|
|
7
|
+
## What is layout thrashing?
|
|
8
|
+
|
|
9
|
+
Layout thrashing occurs when JavaScript reads a layout property (like `offsetWidth` or `getBoundingClientRect`) immediately after writing to styles in the same frame. This forces the browser to perform a synchronous reflow — one of the most expensive operations in the rendering pipeline.
|
|
10
|
+
|
|
11
|
+
```js
|
|
12
|
+
// Bad — triggers forced reflow
|
|
13
|
+
el.style.width = '100px'
|
|
14
|
+
const w = el.offsetWidth // browser must recalculate layout NOW
|
|
15
|
+
|
|
16
|
+
// Worse — N forced reflows in a loop
|
|
17
|
+
for (const box of boxes) {
|
|
18
|
+
box.style.width = box.offsetWidth + 10 + 'px'
|
|
19
|
+
}
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Installation
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
npm install bye-thrash
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Quick Start
|
|
29
|
+
|
|
30
|
+
```jsx
|
|
31
|
+
'use client'
|
|
32
|
+
|
|
33
|
+
import { useEffect } from 'react'
|
|
34
|
+
import { thrash } from 'bye-thrash'
|
|
35
|
+
|
|
36
|
+
export function ThrashMonitor() {
|
|
37
|
+
useEffect(() => {
|
|
38
|
+
thrash.init()
|
|
39
|
+
return () => thrash.destroy()
|
|
40
|
+
}, [])
|
|
41
|
+
|
|
42
|
+
return null
|
|
43
|
+
}
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Drop `<ThrashMonitor />` into your app layout. Open DevTools console — any layout thrashing will log warnings like:
|
|
47
|
+
|
|
48
|
+
```
|
|
49
|
+
[thrash] Layout read "offsetWidth" after style write. Call site: handleResize (src/components/Grid.tsx:42:8)
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## API
|
|
53
|
+
|
|
54
|
+
### `thrash.init()`
|
|
55
|
+
|
|
56
|
+
Patches browser APIs and begins detecting layout thrashing. Automatically skipped in production (`NODE_ENV === 'production'`) unless explicitly enabled via `configure({ enabled: true })`.
|
|
57
|
+
|
|
58
|
+
### `thrash.destroy()`
|
|
59
|
+
|
|
60
|
+
Restores all patched APIs to their originals, cancels the internal frame loop, and clears all recorded data. Safe to call multiple times.
|
|
61
|
+
|
|
62
|
+
### `thrash.configure(options)`
|
|
63
|
+
|
|
64
|
+
Configure behavior before or after calling `init()`.
|
|
65
|
+
|
|
66
|
+
```ts
|
|
67
|
+
thrash.configure({
|
|
68
|
+
mode: 'warn', // 'warn' | 'throw' | 'silent'
|
|
69
|
+
ignorePatterns: [], // (string | RegExp)[] — stack traces matching these are ignored
|
|
70
|
+
autoRaf: true, // whether bye-thrash manages its own requestAnimationFrame loop
|
|
71
|
+
})
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
| Option | Default | Description |
|
|
75
|
+
|--------|---------|-------------|
|
|
76
|
+
| `mode` | `'warn'` | `'warn'` logs to console, `'throw'` throws an error, `'silent'` records without output |
|
|
77
|
+
| `ignorePatterns` | `[]` | Stack trace patterns to ignore — useful for known third-party thrashing you can't fix |
|
|
78
|
+
| `autoRaf` | `true` | When `true`, bye-thrash runs its own rAF loop to reset frame state. Set to `false` if you manage frame timing yourself (see [Manual tick](#manual-tick)) |
|
|
79
|
+
|
|
80
|
+
### `thrash.tick()`
|
|
81
|
+
|
|
82
|
+
Manually resets the frame state (`dirtyFrame` and per-frame read tracking). Only needed when `autoRaf` is `false`.
|
|
83
|
+
|
|
84
|
+
### `thrash.report(options?)`
|
|
85
|
+
|
|
86
|
+
Returns an array of `ReportEntry` objects for all detected thrashing, sorted by count (highest first). Also logs a `console.table` (unless mode is `'silent'`).
|
|
87
|
+
|
|
88
|
+
```ts
|
|
89
|
+
const entries = thrash.report()
|
|
90
|
+
// [{ prop: 'offsetWidth', count: 12, callSite: '...', lastSeen: 1711600000000 }]
|
|
91
|
+
|
|
92
|
+
// Keep accumulated data (don't clear after reporting)
|
|
93
|
+
thrash.report({ clear: false })
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## What gets detected
|
|
97
|
+
|
|
98
|
+
### Writes (mark frame as dirty)
|
|
99
|
+
|
|
100
|
+
| API | Example |
|
|
101
|
+
|-----|---------|
|
|
102
|
+
| Any `el.style.*` assignment | `el.style.width = '100px'` |
|
|
103
|
+
| `style.setProperty()` | `el.style.setProperty('--x', '10')` |
|
|
104
|
+
| `setAttribute` for `style` / `class` | `el.setAttribute('class', 'foo')` |
|
|
105
|
+
| `classList.*` | `el.classList.add('active')` |
|
|
106
|
+
| `className` | `el.className = 'foo bar'` |
|
|
107
|
+
|
|
108
|
+
### Reads (trigger warning if frame is dirty)
|
|
109
|
+
|
|
110
|
+
| API | Example |
|
|
111
|
+
|-----|---------|
|
|
112
|
+
| `offsetWidth/Height/Top/Left` | `el.offsetWidth` |
|
|
113
|
+
| `clientWidth/Height/Top/Left` | `el.clientHeight` |
|
|
114
|
+
| `scrollWidth/Height/Top/Left` | `el.scrollTop` |
|
|
115
|
+
| `getBoundingClientRect()` | `el.getBoundingClientRect()` |
|
|
116
|
+
| `getClientRects()` | `el.getClientRects()` |
|
|
117
|
+
| `getComputedStyle()` | `window.getComputedStyle(el)` |
|
|
118
|
+
| `innerText` | `el.innerText` |
|
|
119
|
+
| `window.innerWidth/Height` | `window.innerWidth` |
|
|
120
|
+
| `window.scrollX/Y` | `window.scrollY` |
|
|
121
|
+
|
|
122
|
+
## Manual tick
|
|
123
|
+
|
|
124
|
+
If you use a library like [tempus](https://github.com/darkroom-engineering/tempus) to orchestrate `requestAnimationFrame` callback order, bye-thrash's internal rAF loop may fire at the wrong time relative to your code — making the dirty/clean frame boundary unreliable.
|
|
125
|
+
|
|
126
|
+
Disable the internal loop and call `tick()` yourself at the start of each frame:
|
|
127
|
+
|
|
128
|
+
```ts
|
|
129
|
+
import { thrash } from 'bye-thrash'
|
|
130
|
+
import tempus from 'tempus'
|
|
131
|
+
|
|
132
|
+
thrash.configure({ autoRaf: false })
|
|
133
|
+
thrash.init()
|
|
134
|
+
|
|
135
|
+
// Reset frame state at the very start of each frame
|
|
136
|
+
tempus.add((time, delta) => {
|
|
137
|
+
thrash.tick()
|
|
138
|
+
}, 0) // priority 0 = runs first
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
## How to fix thrashing
|
|
142
|
+
|
|
143
|
+
**Read first, write second:**
|
|
144
|
+
|
|
145
|
+
```js
|
|
146
|
+
const w = el.offsetWidth // read (clean frame)
|
|
147
|
+
el.style.width = w + 50 + 'px' // write
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
**Batch reads and writes separately:**
|
|
151
|
+
|
|
152
|
+
```js
|
|
153
|
+
// All reads
|
|
154
|
+
const widths = boxes.map(b => b.offsetWidth)
|
|
155
|
+
|
|
156
|
+
// All writes
|
|
157
|
+
boxes.forEach((b, i) => {
|
|
158
|
+
b.style.width = widths[i] + 10 + 'px'
|
|
159
|
+
})
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
**Defer reads to the next frame:**
|
|
163
|
+
|
|
164
|
+
```js
|
|
165
|
+
el.style.width = '200px'
|
|
166
|
+
requestAnimationFrame(() => {
|
|
167
|
+
const rect = el.getBoundingClientRect() // safe — new frame
|
|
168
|
+
})
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
## Types
|
|
172
|
+
|
|
173
|
+
```ts
|
|
174
|
+
import type { ThrashConfig, ReportEntry, ReportOptions } from 'bye-thrash'
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
<br/>
|
|
178
|
+
|
|
179
|
+
## Version Management
|
|
180
|
+
|
|
181
|
+
This library uses [Changesets](https://github.com/changesets/changesets) to manage versions and publish releases.
|
|
182
|
+
|
|
183
|
+
### Adding a changeset
|
|
184
|
+
|
|
185
|
+
When you make changes that need to be released:
|
|
186
|
+
|
|
187
|
+
```bash
|
|
188
|
+
pnpm changeset
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
This will prompt you to:
|
|
192
|
+
|
|
193
|
+
1. Select which packages you want to include in the changeset
|
|
194
|
+
2. Choose whether it's a major/minor/patch bump
|
|
195
|
+
3. Provide a summary of the changes
|
|
196
|
+
|
|
197
|
+
### Creating a release
|
|
198
|
+
|
|
199
|
+
```bash
|
|
200
|
+
# 1. Create new versions of packages
|
|
201
|
+
pnpm version:package
|
|
202
|
+
|
|
203
|
+
# 2. Release (builds and publishes to npm)
|
|
204
|
+
pnpm release
|
|
205
|
+
```
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
type Mode = 'warn' | 'throw' | 'silent';
|
|
2
|
+
interface ThrashConfig {
|
|
3
|
+
mode: Mode;
|
|
4
|
+
ignorePatterns: (string | RegExp)[];
|
|
5
|
+
enabled: boolean;
|
|
6
|
+
autoRaf: boolean;
|
|
7
|
+
}
|
|
8
|
+
interface ReportEntry {
|
|
9
|
+
prop: string;
|
|
10
|
+
count: number;
|
|
11
|
+
callSite: string;
|
|
12
|
+
lastSeen: number;
|
|
13
|
+
}
|
|
14
|
+
interface ReportOptions {
|
|
15
|
+
clear?: boolean;
|
|
16
|
+
}
|
|
17
|
+
declare class Thrash {
|
|
18
|
+
private config;
|
|
19
|
+
private dirtyFrame;
|
|
20
|
+
private rafId;
|
|
21
|
+
private frameReads;
|
|
22
|
+
private offenders;
|
|
23
|
+
private styleProxyCache;
|
|
24
|
+
private restoreFns;
|
|
25
|
+
configure(options: Partial<ThrashConfig>): void;
|
|
26
|
+
init(): void;
|
|
27
|
+
destroy(): void;
|
|
28
|
+
tick(): void;
|
|
29
|
+
report(options?: ReportOptions): ReportEntry[];
|
|
30
|
+
private onLayoutRead;
|
|
31
|
+
private extractCallSite;
|
|
32
|
+
private shouldIgnore;
|
|
33
|
+
private isProduction;
|
|
34
|
+
private patchMethod;
|
|
35
|
+
private patchGetter;
|
|
36
|
+
private patchSetter;
|
|
37
|
+
private lookupDescriptor;
|
|
38
|
+
private patchWrites;
|
|
39
|
+
private patchReads;
|
|
40
|
+
}
|
|
41
|
+
declare const thrash: Thrash;
|
|
42
|
+
|
|
43
|
+
declare const VERSION: string;
|
|
44
|
+
|
|
45
|
+
export { type ReportEntry, type ReportOptions, Thrash, type ThrashConfig, VERSION, thrash };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
type Mode = 'warn' | 'throw' | 'silent';
|
|
2
|
+
interface ThrashConfig {
|
|
3
|
+
mode: Mode;
|
|
4
|
+
ignorePatterns: (string | RegExp)[];
|
|
5
|
+
enabled: boolean;
|
|
6
|
+
autoRaf: boolean;
|
|
7
|
+
}
|
|
8
|
+
interface ReportEntry {
|
|
9
|
+
prop: string;
|
|
10
|
+
count: number;
|
|
11
|
+
callSite: string;
|
|
12
|
+
lastSeen: number;
|
|
13
|
+
}
|
|
14
|
+
interface ReportOptions {
|
|
15
|
+
clear?: boolean;
|
|
16
|
+
}
|
|
17
|
+
declare class Thrash {
|
|
18
|
+
private config;
|
|
19
|
+
private dirtyFrame;
|
|
20
|
+
private rafId;
|
|
21
|
+
private frameReads;
|
|
22
|
+
private offenders;
|
|
23
|
+
private styleProxyCache;
|
|
24
|
+
private restoreFns;
|
|
25
|
+
configure(options: Partial<ThrashConfig>): void;
|
|
26
|
+
init(): void;
|
|
27
|
+
destroy(): void;
|
|
28
|
+
tick(): void;
|
|
29
|
+
report(options?: ReportOptions): ReportEntry[];
|
|
30
|
+
private onLayoutRead;
|
|
31
|
+
private extractCallSite;
|
|
32
|
+
private shouldIgnore;
|
|
33
|
+
private isProduction;
|
|
34
|
+
private patchMethod;
|
|
35
|
+
private patchGetter;
|
|
36
|
+
private patchSetter;
|
|
37
|
+
private lookupDescriptor;
|
|
38
|
+
private patchWrites;
|
|
39
|
+
private patchReads;
|
|
40
|
+
}
|
|
41
|
+
declare const thrash: Thrash;
|
|
42
|
+
|
|
43
|
+
declare const VERSION: string;
|
|
44
|
+
|
|
45
|
+
export { type ReportEntry, type ReportOptions, Thrash, type ThrashConfig, VERSION, thrash };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// package.json
|
|
4
|
+
var version = "0.0.1";
|
|
5
|
+
|
|
6
|
+
// packages/core/core.tsx
|
|
7
|
+
var LIB_FILENAME = "thrash";
|
|
8
|
+
var USER_CODE_RE = /\.(tsx?|jsx?|mjs|cjs)[:)]/;
|
|
9
|
+
var FRAMEWORK_RE = /react-dom|react-reconciler|react-server|scheduler|__next|next\/dist|_next\/static\/chunks|webpack|turbopack|hmr-runtime|chunk-/;
|
|
10
|
+
var activeInstance = null;
|
|
11
|
+
var Thrash = class {
|
|
12
|
+
constructor() {
|
|
13
|
+
this.config = {
|
|
14
|
+
mode: "warn",
|
|
15
|
+
ignorePatterns: [],
|
|
16
|
+
enabled: false,
|
|
17
|
+
autoRaf: true
|
|
18
|
+
};
|
|
19
|
+
this.dirtyFrame = false;
|
|
20
|
+
this.rafId = null;
|
|
21
|
+
this.frameReads = [];
|
|
22
|
+
this.offenders = /* @__PURE__ */ new Map();
|
|
23
|
+
this.styleProxyCache = /* @__PURE__ */ new WeakMap();
|
|
24
|
+
this.restoreFns = [];
|
|
25
|
+
}
|
|
26
|
+
configure(options) {
|
|
27
|
+
Object.assign(this.config, options);
|
|
28
|
+
}
|
|
29
|
+
init() {
|
|
30
|
+
if (this.isProduction() && !this.config.enabled) return;
|
|
31
|
+
if (this.config.enabled) return;
|
|
32
|
+
if (activeInstance && activeInstance !== this) {
|
|
33
|
+
throw new Error("[thrash] Another instance is already active. Call destroy() on it first.");
|
|
34
|
+
}
|
|
35
|
+
this.config.enabled = true;
|
|
36
|
+
this.dirtyFrame = false;
|
|
37
|
+
this.frameReads = [];
|
|
38
|
+
this.patchWrites();
|
|
39
|
+
this.patchReads();
|
|
40
|
+
activeInstance = this;
|
|
41
|
+
if (this.config.autoRaf) {
|
|
42
|
+
this.rafId = requestAnimationFrame(() => this.tick());
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
destroy() {
|
|
46
|
+
this.config.enabled = false;
|
|
47
|
+
for (const restore of this.restoreFns) {
|
|
48
|
+
try {
|
|
49
|
+
restore();
|
|
50
|
+
} catch {
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
this.restoreFns.length = 0;
|
|
54
|
+
if (this.rafId !== null) {
|
|
55
|
+
cancelAnimationFrame(this.rafId);
|
|
56
|
+
this.rafId = null;
|
|
57
|
+
}
|
|
58
|
+
this.offenders.clear();
|
|
59
|
+
this.frameReads = [];
|
|
60
|
+
activeInstance = null;
|
|
61
|
+
}
|
|
62
|
+
tick() {
|
|
63
|
+
this.dirtyFrame = false;
|
|
64
|
+
this.frameReads = [];
|
|
65
|
+
if (this.config.autoRaf) {
|
|
66
|
+
this.rafId = requestAnimationFrame(() => this.tick());
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
report(options = {}) {
|
|
70
|
+
const { clear = true } = options;
|
|
71
|
+
const entries = [];
|
|
72
|
+
for (const [, offender] of this.offenders) {
|
|
73
|
+
entries.push({
|
|
74
|
+
prop: offender.prop,
|
|
75
|
+
count: offender.count,
|
|
76
|
+
callSite: offender.callSite,
|
|
77
|
+
lastSeen: offender.lastSeen
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
entries.sort((a, b) => b.count - a.count);
|
|
81
|
+
if (entries.length > 0) {
|
|
82
|
+
console.table(entries);
|
|
83
|
+
} else {
|
|
84
|
+
console.log("[thrash] No layout thrashing detected.");
|
|
85
|
+
}
|
|
86
|
+
if (clear) this.offenders.clear();
|
|
87
|
+
return entries;
|
|
88
|
+
}
|
|
89
|
+
// -------------------------------------------------------------------------
|
|
90
|
+
// Detection
|
|
91
|
+
// -------------------------------------------------------------------------
|
|
92
|
+
onLayoutRead(prop) {
|
|
93
|
+
if (!this.config.enabled) return;
|
|
94
|
+
const stack = new Error().stack ?? "";
|
|
95
|
+
if (this.shouldIgnore(stack)) return;
|
|
96
|
+
this.frameReads.push({ prop, stack, timestamp: performance.now() });
|
|
97
|
+
if (!this.dirtyFrame) return;
|
|
98
|
+
const callSite = this.extractCallSite(stack);
|
|
99
|
+
const key = `${prop}::${callSite}`;
|
|
100
|
+
const existing = this.offenders.get(key);
|
|
101
|
+
if (existing) {
|
|
102
|
+
existing.count++;
|
|
103
|
+
existing.lastSeen = Date.now();
|
|
104
|
+
} else {
|
|
105
|
+
this.offenders.set(key, {
|
|
106
|
+
count: 1,
|
|
107
|
+
prop,
|
|
108
|
+
callSite,
|
|
109
|
+
stack,
|
|
110
|
+
lastSeen: Date.now()
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
const message = `[thrash] Layout read "${prop}" after style write. Call site: ${callSite}`;
|
|
114
|
+
if (this.config.mode === "throw") {
|
|
115
|
+
throw new Error(message);
|
|
116
|
+
} else if (this.config.mode === "warn") {
|
|
117
|
+
console.warn(message);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
extractCallSite(stack) {
|
|
121
|
+
const lines = stack.split("\n").slice(1);
|
|
122
|
+
for (const line of lines) {
|
|
123
|
+
const trimmed = line.trim();
|
|
124
|
+
if (trimmed.includes(LIB_FILENAME)) continue;
|
|
125
|
+
if (trimmed.includes("node_modules")) continue;
|
|
126
|
+
if (FRAMEWORK_RE.test(trimmed)) continue;
|
|
127
|
+
if (USER_CODE_RE.test(trimmed)) {
|
|
128
|
+
return trimmed.replace(/^\s*at\s+/, "");
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
for (const line of lines) {
|
|
132
|
+
const trimmed = line.trim();
|
|
133
|
+
if (trimmed.includes(LIB_FILENAME)) continue;
|
|
134
|
+
if (trimmed.includes("node_modules")) continue;
|
|
135
|
+
if (FRAMEWORK_RE.test(trimmed)) continue;
|
|
136
|
+
return trimmed.replace(/^\s*at\s+/, "");
|
|
137
|
+
}
|
|
138
|
+
for (const line of lines) {
|
|
139
|
+
const trimmed = line.trim();
|
|
140
|
+
if (trimmed.includes(LIB_FILENAME)) continue;
|
|
141
|
+
return trimmed.replace(/^\s*at\s+/, "");
|
|
142
|
+
}
|
|
143
|
+
return "unknown";
|
|
144
|
+
}
|
|
145
|
+
shouldIgnore(stack) {
|
|
146
|
+
return this.config.ignorePatterns.some((pattern) => {
|
|
147
|
+
if (typeof pattern === "string") return stack.includes(pattern);
|
|
148
|
+
return pattern.test(stack);
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
isProduction() {
|
|
152
|
+
try {
|
|
153
|
+
return process.env.NODE_ENV === "production";
|
|
154
|
+
} catch {
|
|
155
|
+
return false;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
patchMethod(target, methodName, wrapper) {
|
|
159
|
+
const original = target[methodName];
|
|
160
|
+
if (typeof original !== "function") return;
|
|
161
|
+
const patched = wrapper(original);
|
|
162
|
+
target[methodName] = patched;
|
|
163
|
+
this.restoreFns.push(() => {
|
|
164
|
+
target[methodName] = original;
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
patchGetter(target, prop, onGet) {
|
|
168
|
+
const descriptor = this.lookupDescriptor(target, prop);
|
|
169
|
+
if (!descriptor?.get) return;
|
|
170
|
+
const originalGet = descriptor.get;
|
|
171
|
+
Object.defineProperty(target, prop, {
|
|
172
|
+
...descriptor,
|
|
173
|
+
get() {
|
|
174
|
+
onGet(prop);
|
|
175
|
+
return originalGet.call(this);
|
|
176
|
+
}
|
|
177
|
+
});
|
|
178
|
+
this.restoreFns.push(() => {
|
|
179
|
+
Object.defineProperty(target, prop, descriptor);
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
patchSetter(target, prop, onSet) {
|
|
183
|
+
const descriptor = this.lookupDescriptor(target, prop);
|
|
184
|
+
if (!descriptor?.set) return;
|
|
185
|
+
const originalSet = descriptor.set;
|
|
186
|
+
const originalGet = descriptor.get;
|
|
187
|
+
Object.defineProperty(target, prop, {
|
|
188
|
+
...descriptor,
|
|
189
|
+
get: originalGet ? function() {
|
|
190
|
+
return originalGet.call(this);
|
|
191
|
+
} : void 0,
|
|
192
|
+
set(value) {
|
|
193
|
+
onSet();
|
|
194
|
+
originalSet.call(this, value);
|
|
195
|
+
}
|
|
196
|
+
});
|
|
197
|
+
this.restoreFns.push(() => {
|
|
198
|
+
Object.defineProperty(target, prop, descriptor);
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
lookupDescriptor(target, prop) {
|
|
202
|
+
let current = target;
|
|
203
|
+
while (current) {
|
|
204
|
+
const desc = Object.getOwnPropertyDescriptor(current, prop);
|
|
205
|
+
if (desc) return desc;
|
|
206
|
+
current = Object.getPrototypeOf(current);
|
|
207
|
+
}
|
|
208
|
+
return void 0;
|
|
209
|
+
}
|
|
210
|
+
patchWrites() {
|
|
211
|
+
const markDirty = () => {
|
|
212
|
+
this.dirtyFrame = true;
|
|
213
|
+
};
|
|
214
|
+
this.patchMethod(CSSStyleDeclaration.prototype, "setProperty", (orig) => {
|
|
215
|
+
return function(...args) {
|
|
216
|
+
markDirty();
|
|
217
|
+
return orig.apply(this, args);
|
|
218
|
+
};
|
|
219
|
+
});
|
|
220
|
+
this.patchMethod(Element.prototype, "setAttribute", (orig) => {
|
|
221
|
+
return function(...args) {
|
|
222
|
+
const name = args[0]?.toLowerCase?.();
|
|
223
|
+
if (name === "style" || name === "class") markDirty();
|
|
224
|
+
return orig.apply(this, args);
|
|
225
|
+
};
|
|
226
|
+
});
|
|
227
|
+
const classListMethods = ["add", "remove", "toggle", "replace"];
|
|
228
|
+
for (const method of classListMethods) {
|
|
229
|
+
this.patchMethod(DOMTokenList.prototype, method, (orig) => {
|
|
230
|
+
return function(...args) {
|
|
231
|
+
markDirty();
|
|
232
|
+
return orig.apply(this, args);
|
|
233
|
+
};
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
this.patchSetter(HTMLElement.prototype, "className", markDirty);
|
|
237
|
+
const styleDescriptor = this.lookupDescriptor(HTMLElement.prototype, "style");
|
|
238
|
+
if (styleDescriptor?.get) {
|
|
239
|
+
const originalStyleGet = styleDescriptor.get;
|
|
240
|
+
const config = this.config;
|
|
241
|
+
const cache = this.styleProxyCache;
|
|
242
|
+
Object.defineProperty(HTMLElement.prototype, "style", {
|
|
243
|
+
...styleDescriptor,
|
|
244
|
+
get() {
|
|
245
|
+
const real = originalStyleGet.call(this);
|
|
246
|
+
if (!config.enabled) return real;
|
|
247
|
+
const cached = cache.get(this);
|
|
248
|
+
if (cached) return cached;
|
|
249
|
+
const proxy = new Proxy(real, {
|
|
250
|
+
set(target, prop, value) {
|
|
251
|
+
markDirty();
|
|
252
|
+
return Reflect.set(target, prop, value, target);
|
|
253
|
+
}
|
|
254
|
+
});
|
|
255
|
+
cache.set(this, proxy);
|
|
256
|
+
return proxy;
|
|
257
|
+
}
|
|
258
|
+
});
|
|
259
|
+
this.restoreFns.push(() => {
|
|
260
|
+
Object.defineProperty(HTMLElement.prototype, "style", styleDescriptor);
|
|
261
|
+
this.styleProxyCache = /* @__PURE__ */ new WeakMap();
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
patchReads() {
|
|
266
|
+
const onRead = (prop) => this.onLayoutRead(prop);
|
|
267
|
+
const elementReadProps = [
|
|
268
|
+
"offsetWidth",
|
|
269
|
+
"offsetHeight",
|
|
270
|
+
"offsetTop",
|
|
271
|
+
"offsetLeft",
|
|
272
|
+
"offsetParent",
|
|
273
|
+
"clientWidth",
|
|
274
|
+
"clientHeight",
|
|
275
|
+
"clientTop",
|
|
276
|
+
"clientLeft",
|
|
277
|
+
"scrollWidth",
|
|
278
|
+
"scrollHeight",
|
|
279
|
+
"scrollTop",
|
|
280
|
+
"scrollLeft"
|
|
281
|
+
];
|
|
282
|
+
for (const prop of elementReadProps) {
|
|
283
|
+
this.patchGetter(HTMLElement.prototype, prop, onRead);
|
|
284
|
+
}
|
|
285
|
+
this.patchMethod(Element.prototype, "getBoundingClientRect", (orig) => {
|
|
286
|
+
return function() {
|
|
287
|
+
onRead("getBoundingClientRect");
|
|
288
|
+
return orig.call(this);
|
|
289
|
+
};
|
|
290
|
+
});
|
|
291
|
+
this.patchMethod(Element.prototype, "getClientRects", (orig) => {
|
|
292
|
+
return function() {
|
|
293
|
+
onRead("getClientRects");
|
|
294
|
+
return orig.call(this);
|
|
295
|
+
};
|
|
296
|
+
});
|
|
297
|
+
this.patchGetter(HTMLElement.prototype, "innerText", onRead);
|
|
298
|
+
const windowReadProps = ["innerWidth", "innerHeight", "scrollX", "scrollY"];
|
|
299
|
+
for (const prop of windowReadProps) {
|
|
300
|
+
this.patchGetter(window, prop, onRead);
|
|
301
|
+
}
|
|
302
|
+
this.patchMethod(window, "getComputedStyle", (orig) => {
|
|
303
|
+
return function(...args) {
|
|
304
|
+
onRead("getComputedStyle");
|
|
305
|
+
return orig.apply(this, args);
|
|
306
|
+
};
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
};
|
|
310
|
+
var thrash = new Thrash();
|
|
311
|
+
|
|
312
|
+
// packages/core/index.ts
|
|
313
|
+
var VERSION = version;
|
|
314
|
+
|
|
315
|
+
exports.Thrash = Thrash;
|
|
316
|
+
exports.VERSION = VERSION;
|
|
317
|
+
exports.thrash = thrash;
|
|
318
|
+
//# sourceMappingURL=index.js.map
|
|
319
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../package.json","../packages/core/core.tsx","../packages/core/index.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["{\n \"name\": \"bye-thrash\",\n \"publishConfig\": {\n \"registry\": \"https://registry.npmjs.org\",\n \"access\": \"public\"\n },\n \"version\": \"0.0.1\",\n \"description\": \"Detect and destroy layout thrashing\",\n \"main\": \"dist/index.js\",\n \"module\": \"dist/index.mjs\",\n \"types\": \"dist/index.d.ts\",\n \"files\": [\n \"dist\"\n ],\n \"exports\": {\n \".\": {\n \"types\": \"./dist/index.d.ts\",\n \"default\": \"./dist/index.mjs\"\n }\n },\n \"scripts\": {\n \"build\": \"tsup\",\n \"dev\": \"concurrently \\\"tsup --watch\\\" \\\"cd templates/demo && pnpm dev\\\"\",\n \"typecheck\": \"tsc --noEmit\",\n \"version:package\": \"pnpm changeset version\",\n \"release\": \"pnpm build && pnpm changeset publish\",\n \"lint\": \"eslint -c ./eslint.config.mjs . --fix --no-cache\"\n },\n \"author\": \"joyco.studio\",\n \"license\": \"ISC\",\n \"packageManager\": \"pnpm@10.29.2\",\n \"devDependencies\": {\n \"@changesets/cli\": \"^2.27.11\",\n \"@eslint/js\": \"^9.18.0\",\n \"@types/node\": \"^20.11.24\",\n \"@typescript-eslint/eslint-plugin\": \"^8.21.0\",\n \"@typescript-eslint/parser\": \"^8.21.0\",\n \"eslint\": \"^9.18.0\",\n \"eslint-config-prettier\": \"^10.0.1\",\n \"eslint-plugin-prettier\": \"^5.2.3\",\n \"eslint-plugin-react\": \"^7.37.4\",\n \"eslint-plugin-react-compiler\": \"19.0.0-beta-decd7b8-20250118\",\n \"globals\": \"^15.14.0\",\n \"prettier\": \"^3.4.2\",\n \"tsup\": \"^8.0.2\",\n \"typescript\": \"^5.7.3\",\n \"typescript-eslint\": \"^8.21.0\"\n },\n \"dependencies\": {\n \"concurrently\": \"^9.2.1\"\n }\n}\n","type Mode = 'warn' | 'throw' | 'silent'\n\ninterface ThrashConfig {\n mode: Mode\n ignorePatterns: (string | RegExp)[]\n enabled: boolean\n autoRaf: boolean\n}\n\ninterface Offender {\n count: number\n prop: string\n callSite: string\n stack: string\n lastSeen: number\n}\n\ninterface FrameRead {\n prop: string\n stack: string\n timestamp: number\n}\n\ninterface ReportEntry {\n prop: string\n count: number\n callSite: string\n lastSeen: number\n}\n\ninterface ReportOptions {\n clear?: boolean\n}\n\nconst LIB_FILENAME = 'thrash'\nconst USER_CODE_RE = /\\.(tsx?|jsx?|mjs|cjs)[:)]/\nconst FRAMEWORK_RE =\n /react-dom|react-reconciler|react-server|scheduler|__next|next\\/dist|_next\\/static\\/chunks|webpack|turbopack|hmr-runtime|chunk-/\n\nlet activeInstance: Thrash | null = null\n\nclass Thrash {\n private config: ThrashConfig = {\n mode: 'warn',\n ignorePatterns: [],\n enabled: false,\n autoRaf: true,\n }\n\n private dirtyFrame = false\n private rafId: number | null = null\n private frameReads: FrameRead[] = []\n private offenders = new Map<string, Offender>()\n private styleProxyCache = new WeakMap<HTMLElement, CSSStyleDeclaration>()\n private restoreFns: Array<() => void> = []\n\n configure(options: Partial<ThrashConfig>): void {\n Object.assign(this.config, options)\n }\n\n init(): void {\n if (this.isProduction() && !this.config.enabled) return\n if (this.config.enabled) return\n if (activeInstance && activeInstance !== this) {\n throw new Error('[thrash] Another instance is already active. Call destroy() on it first.')\n }\n\n this.config.enabled = true\n this.dirtyFrame = false\n this.frameReads = []\n\n this.patchWrites()\n this.patchReads()\n // eslint-disable-next-line @typescript-eslint/no-this-alias\n activeInstance = this\n\n if (this.config.autoRaf) {\n this.rafId = requestAnimationFrame(() => this.tick())\n }\n }\n\n destroy(): void {\n this.config.enabled = false\n\n for (const restore of this.restoreFns) {\n try {\n restore()\n } catch {\n // Descriptor may already be gone\n }\n }\n this.restoreFns.length = 0\n\n if (this.rafId !== null) {\n cancelAnimationFrame(this.rafId)\n this.rafId = null\n }\n\n this.offenders.clear()\n this.frameReads = []\n activeInstance = null\n }\n\n tick(): void {\n this.dirtyFrame = false\n this.frameReads = []\n if (this.config.autoRaf) {\n this.rafId = requestAnimationFrame(() => this.tick())\n }\n }\n\n report(options: ReportOptions = {}): ReportEntry[] {\n const { clear = true } = options\n\n const entries: ReportEntry[] = []\n for (const [, offender] of this.offenders) {\n entries.push({\n prop: offender.prop,\n count: offender.count,\n callSite: offender.callSite,\n lastSeen: offender.lastSeen,\n })\n }\n\n entries.sort((a, b) => b.count - a.count)\n\n if (entries.length > 0) {\n console.table(entries)\n } else {\n console.log('[thrash] No layout thrashing detected.')\n }\n\n if (clear) this.offenders.clear()\n\n return entries\n }\n\n // -------------------------------------------------------------------------\n // Detection\n // -------------------------------------------------------------------------\n\n private onLayoutRead(prop: string): void {\n if (!this.config.enabled) return\n\n const stack = new Error().stack ?? ''\n\n if (this.shouldIgnore(stack)) return\n\n this.frameReads.push({ prop, stack, timestamp: performance.now() })\n\n if (!this.dirtyFrame) return\n\n const callSite = this.extractCallSite(stack)\n const key = `${prop}::${callSite}`\n const existing = this.offenders.get(key)\n\n if (existing) {\n existing.count++\n existing.lastSeen = Date.now()\n } else {\n this.offenders.set(key, {\n count: 1,\n prop,\n callSite,\n stack,\n lastSeen: Date.now(),\n })\n }\n\n const message = `[thrash] Layout read \"${prop}\" after style write. Call site: ${callSite}`\n\n if (this.config.mode === 'throw') {\n throw new Error(message)\n } else if (this.config.mode === 'warn') {\n console.warn(message)\n }\n }\n\n private extractCallSite(stack: string): string {\n const lines = stack.split('\\n').slice(1)\n\n for (const line of lines) {\n const trimmed = line.trim()\n if (trimmed.includes(LIB_FILENAME)) continue\n if (trimmed.includes('node_modules')) continue\n if (FRAMEWORK_RE.test(trimmed)) continue\n if (USER_CODE_RE.test(trimmed)) {\n return trimmed.replace(/^\\s*at\\s+/, '')\n }\n }\n\n for (const line of lines) {\n const trimmed = line.trim()\n if (trimmed.includes(LIB_FILENAME)) continue\n if (trimmed.includes('node_modules')) continue\n if (FRAMEWORK_RE.test(trimmed)) continue\n return trimmed.replace(/^\\s*at\\s+/, '')\n }\n\n for (const line of lines) {\n const trimmed = line.trim()\n if (trimmed.includes(LIB_FILENAME)) continue\n return trimmed.replace(/^\\s*at\\s+/, '')\n }\n\n return 'unknown'\n }\n\n private shouldIgnore(stack: string): boolean {\n return this.config.ignorePatterns.some((pattern) => {\n if (typeof pattern === 'string') return stack.includes(pattern)\n return pattern.test(stack)\n })\n }\n\n private isProduction(): boolean {\n try {\n return process.env.NODE_ENV === 'production'\n } catch {\n return false\n }\n }\n\n private patchMethod<T extends object>(\n target: T,\n methodName: string,\n wrapper: (original: (...args: unknown[]) => unknown) => (...args: unknown[]) => unknown\n ): void {\n const original = (target as Record<string, unknown>)[methodName] as (...args: unknown[]) => unknown\n if (typeof original !== 'function') return\n\n const patched = wrapper(original)\n ;(target as Record<string, unknown>)[methodName] = patched\n\n this.restoreFns.push(() => {\n ;(target as Record<string, unknown>)[methodName] = original\n })\n }\n\n private patchGetter<T extends object>(target: T, prop: string, onGet: (prop: string) => void): void {\n const descriptor = this.lookupDescriptor(target, prop)\n if (!descriptor?.get) return\n\n const originalGet = descriptor.get\n\n Object.defineProperty(target, prop, {\n ...descriptor,\n get() {\n onGet(prop)\n return originalGet.call(this)\n },\n })\n\n this.restoreFns.push(() => {\n Object.defineProperty(target, prop, descriptor)\n })\n }\n\n private patchSetter<T extends object>(target: T, prop: string, onSet: () => void): void {\n const descriptor = this.lookupDescriptor(target, prop)\n if (!descriptor?.set) return\n\n const originalSet = descriptor.set\n const originalGet = descriptor.get\n\n Object.defineProperty(target, prop, {\n ...descriptor,\n get: originalGet\n ? function (this: unknown) {\n return originalGet.call(this)\n }\n : undefined,\n set(value: unknown) {\n onSet()\n originalSet.call(this, value)\n },\n })\n\n this.restoreFns.push(() => {\n Object.defineProperty(target, prop, descriptor)\n })\n }\n\n private lookupDescriptor(target: object, prop: string): PropertyDescriptor | undefined {\n let current: object | null = target\n while (current) {\n const desc = Object.getOwnPropertyDescriptor(current, prop)\n if (desc) return desc\n current = Object.getPrototypeOf(current) as object | null\n }\n return undefined\n }\n\n private patchWrites(): void {\n const markDirty = () => {\n this.dirtyFrame = true\n }\n\n this.patchMethod(CSSStyleDeclaration.prototype, 'setProperty', (orig) => {\n return function (this: CSSStyleDeclaration, ...args: unknown[]) {\n markDirty()\n return orig.apply(this, args)\n }\n })\n\n this.patchMethod(Element.prototype, 'setAttribute', (orig) => {\n return function (this: Element, ...args: unknown[]) {\n const name = (args[0] as string)?.toLowerCase?.()\n if (name === 'style' || name === 'class') markDirty()\n return orig.apply(this, args)\n }\n })\n\n const classListMethods = ['add', 'remove', 'toggle', 'replace'] as const\n for (const method of classListMethods) {\n this.patchMethod(DOMTokenList.prototype, method, (orig) => {\n return function (this: DOMTokenList, ...args: unknown[]) {\n markDirty()\n return orig.apply(this, args)\n }\n })\n }\n\n this.patchSetter(HTMLElement.prototype, 'className', markDirty)\n\n // Proxy the `style` getter so any property assignment on the returned\n // CSSStyleDeclaration marks the frame dirty. Browsers implement CSS\n // properties as exotic/internal properties without standard descriptors,\n // so patching individual setters doesn't work.\n const styleDescriptor = this.lookupDescriptor(HTMLElement.prototype, 'style')\n if (styleDescriptor?.get) {\n const originalStyleGet = styleDescriptor.get\n const config = this.config\n const cache = this.styleProxyCache\n\n Object.defineProperty(HTMLElement.prototype, 'style', {\n ...styleDescriptor,\n get(this: HTMLElement) {\n const real = originalStyleGet.call(this) as CSSStyleDeclaration\n if (!config.enabled) return real\n\n const cached = cache.get(this)\n if (cached) return cached\n\n const proxy = new Proxy(real, {\n set(target, prop, value) {\n markDirty()\n return Reflect.set(target, prop, value, target)\n },\n })\n\n cache.set(this, proxy)\n return proxy\n },\n })\n\n this.restoreFns.push(() => {\n Object.defineProperty(HTMLElement.prototype, 'style', styleDescriptor)\n this.styleProxyCache = new WeakMap()\n })\n }\n }\n\n private patchReads(): void {\n const onRead = (prop: string) => this.onLayoutRead(prop)\n\n const elementReadProps = [\n 'offsetWidth',\n 'offsetHeight',\n 'offsetTop',\n 'offsetLeft',\n 'offsetParent',\n 'clientWidth',\n 'clientHeight',\n 'clientTop',\n 'clientLeft',\n 'scrollWidth',\n 'scrollHeight',\n 'scrollTop',\n 'scrollLeft',\n ]\n\n for (const prop of elementReadProps) {\n this.patchGetter(HTMLElement.prototype, prop, onRead)\n }\n\n this.patchMethod(Element.prototype, 'getBoundingClientRect', (orig) => {\n return function (this: Element) {\n onRead('getBoundingClientRect')\n return orig.call(this)\n }\n })\n\n this.patchMethod(Element.prototype, 'getClientRects', (orig) => {\n return function (this: Element) {\n onRead('getClientRects')\n return orig.call(this)\n }\n })\n\n this.patchGetter(HTMLElement.prototype, 'innerText', onRead)\n\n const windowReadProps = ['innerWidth', 'innerHeight', 'scrollX', 'scrollY']\n for (const prop of windowReadProps) {\n this.patchGetter(window, prop, onRead)\n }\n\n this.patchMethod(window, 'getComputedStyle', (orig) => {\n return function (this: typeof window, ...args: unknown[]) {\n onRead('getComputedStyle')\n return orig.apply(this, args)\n }\n })\n }\n}\n\nexport const thrash = new Thrash()\nexport { Thrash }\nexport type { ThrashConfig, ReportEntry, ReportOptions }\n","import { version } from '../../package.json'\n\nexport const VERSION = version\nexport * from './core'\n"]}
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
// package.json
|
|
2
|
+
var version = "0.0.1";
|
|
3
|
+
|
|
4
|
+
// packages/core/core.tsx
|
|
5
|
+
var LIB_FILENAME = "thrash";
|
|
6
|
+
var USER_CODE_RE = /\.(tsx?|jsx?|mjs|cjs)[:)]/;
|
|
7
|
+
var FRAMEWORK_RE = /react-dom|react-reconciler|react-server|scheduler|__next|next\/dist|_next\/static\/chunks|webpack|turbopack|hmr-runtime|chunk-/;
|
|
8
|
+
var activeInstance = null;
|
|
9
|
+
var Thrash = class {
|
|
10
|
+
constructor() {
|
|
11
|
+
this.config = {
|
|
12
|
+
mode: "warn",
|
|
13
|
+
ignorePatterns: [],
|
|
14
|
+
enabled: false,
|
|
15
|
+
autoRaf: true
|
|
16
|
+
};
|
|
17
|
+
this.dirtyFrame = false;
|
|
18
|
+
this.rafId = null;
|
|
19
|
+
this.frameReads = [];
|
|
20
|
+
this.offenders = /* @__PURE__ */ new Map();
|
|
21
|
+
this.styleProxyCache = /* @__PURE__ */ new WeakMap();
|
|
22
|
+
this.restoreFns = [];
|
|
23
|
+
}
|
|
24
|
+
configure(options) {
|
|
25
|
+
Object.assign(this.config, options);
|
|
26
|
+
}
|
|
27
|
+
init() {
|
|
28
|
+
if (this.isProduction() && !this.config.enabled) return;
|
|
29
|
+
if (this.config.enabled) return;
|
|
30
|
+
if (activeInstance && activeInstance !== this) {
|
|
31
|
+
throw new Error("[thrash] Another instance is already active. Call destroy() on it first.");
|
|
32
|
+
}
|
|
33
|
+
this.config.enabled = true;
|
|
34
|
+
this.dirtyFrame = false;
|
|
35
|
+
this.frameReads = [];
|
|
36
|
+
this.patchWrites();
|
|
37
|
+
this.patchReads();
|
|
38
|
+
activeInstance = this;
|
|
39
|
+
if (this.config.autoRaf) {
|
|
40
|
+
this.rafId = requestAnimationFrame(() => this.tick());
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
destroy() {
|
|
44
|
+
this.config.enabled = false;
|
|
45
|
+
for (const restore of this.restoreFns) {
|
|
46
|
+
try {
|
|
47
|
+
restore();
|
|
48
|
+
} catch {
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
this.restoreFns.length = 0;
|
|
52
|
+
if (this.rafId !== null) {
|
|
53
|
+
cancelAnimationFrame(this.rafId);
|
|
54
|
+
this.rafId = null;
|
|
55
|
+
}
|
|
56
|
+
this.offenders.clear();
|
|
57
|
+
this.frameReads = [];
|
|
58
|
+
activeInstance = null;
|
|
59
|
+
}
|
|
60
|
+
tick() {
|
|
61
|
+
this.dirtyFrame = false;
|
|
62
|
+
this.frameReads = [];
|
|
63
|
+
if (this.config.autoRaf) {
|
|
64
|
+
this.rafId = requestAnimationFrame(() => this.tick());
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
report(options = {}) {
|
|
68
|
+
const { clear = true } = options;
|
|
69
|
+
const entries = [];
|
|
70
|
+
for (const [, offender] of this.offenders) {
|
|
71
|
+
entries.push({
|
|
72
|
+
prop: offender.prop,
|
|
73
|
+
count: offender.count,
|
|
74
|
+
callSite: offender.callSite,
|
|
75
|
+
lastSeen: offender.lastSeen
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
entries.sort((a, b) => b.count - a.count);
|
|
79
|
+
if (entries.length > 0) {
|
|
80
|
+
console.table(entries);
|
|
81
|
+
} else {
|
|
82
|
+
console.log("[thrash] No layout thrashing detected.");
|
|
83
|
+
}
|
|
84
|
+
if (clear) this.offenders.clear();
|
|
85
|
+
return entries;
|
|
86
|
+
}
|
|
87
|
+
// -------------------------------------------------------------------------
|
|
88
|
+
// Detection
|
|
89
|
+
// -------------------------------------------------------------------------
|
|
90
|
+
onLayoutRead(prop) {
|
|
91
|
+
if (!this.config.enabled) return;
|
|
92
|
+
const stack = new Error().stack ?? "";
|
|
93
|
+
if (this.shouldIgnore(stack)) return;
|
|
94
|
+
this.frameReads.push({ prop, stack, timestamp: performance.now() });
|
|
95
|
+
if (!this.dirtyFrame) return;
|
|
96
|
+
const callSite = this.extractCallSite(stack);
|
|
97
|
+
const key = `${prop}::${callSite}`;
|
|
98
|
+
const existing = this.offenders.get(key);
|
|
99
|
+
if (existing) {
|
|
100
|
+
existing.count++;
|
|
101
|
+
existing.lastSeen = Date.now();
|
|
102
|
+
} else {
|
|
103
|
+
this.offenders.set(key, {
|
|
104
|
+
count: 1,
|
|
105
|
+
prop,
|
|
106
|
+
callSite,
|
|
107
|
+
stack,
|
|
108
|
+
lastSeen: Date.now()
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
const message = `[thrash] Layout read "${prop}" after style write. Call site: ${callSite}`;
|
|
112
|
+
if (this.config.mode === "throw") {
|
|
113
|
+
throw new Error(message);
|
|
114
|
+
} else if (this.config.mode === "warn") {
|
|
115
|
+
console.warn(message);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
extractCallSite(stack) {
|
|
119
|
+
const lines = stack.split("\n").slice(1);
|
|
120
|
+
for (const line of lines) {
|
|
121
|
+
const trimmed = line.trim();
|
|
122
|
+
if (trimmed.includes(LIB_FILENAME)) continue;
|
|
123
|
+
if (trimmed.includes("node_modules")) continue;
|
|
124
|
+
if (FRAMEWORK_RE.test(trimmed)) continue;
|
|
125
|
+
if (USER_CODE_RE.test(trimmed)) {
|
|
126
|
+
return trimmed.replace(/^\s*at\s+/, "");
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
for (const line of lines) {
|
|
130
|
+
const trimmed = line.trim();
|
|
131
|
+
if (trimmed.includes(LIB_FILENAME)) continue;
|
|
132
|
+
if (trimmed.includes("node_modules")) continue;
|
|
133
|
+
if (FRAMEWORK_RE.test(trimmed)) continue;
|
|
134
|
+
return trimmed.replace(/^\s*at\s+/, "");
|
|
135
|
+
}
|
|
136
|
+
for (const line of lines) {
|
|
137
|
+
const trimmed = line.trim();
|
|
138
|
+
if (trimmed.includes(LIB_FILENAME)) continue;
|
|
139
|
+
return trimmed.replace(/^\s*at\s+/, "");
|
|
140
|
+
}
|
|
141
|
+
return "unknown";
|
|
142
|
+
}
|
|
143
|
+
shouldIgnore(stack) {
|
|
144
|
+
return this.config.ignorePatterns.some((pattern) => {
|
|
145
|
+
if (typeof pattern === "string") return stack.includes(pattern);
|
|
146
|
+
return pattern.test(stack);
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
isProduction() {
|
|
150
|
+
try {
|
|
151
|
+
return process.env.NODE_ENV === "production";
|
|
152
|
+
} catch {
|
|
153
|
+
return false;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
patchMethod(target, methodName, wrapper) {
|
|
157
|
+
const original = target[methodName];
|
|
158
|
+
if (typeof original !== "function") return;
|
|
159
|
+
const patched = wrapper(original);
|
|
160
|
+
target[methodName] = patched;
|
|
161
|
+
this.restoreFns.push(() => {
|
|
162
|
+
target[methodName] = original;
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
patchGetter(target, prop, onGet) {
|
|
166
|
+
const descriptor = this.lookupDescriptor(target, prop);
|
|
167
|
+
if (!descriptor?.get) return;
|
|
168
|
+
const originalGet = descriptor.get;
|
|
169
|
+
Object.defineProperty(target, prop, {
|
|
170
|
+
...descriptor,
|
|
171
|
+
get() {
|
|
172
|
+
onGet(prop);
|
|
173
|
+
return originalGet.call(this);
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
this.restoreFns.push(() => {
|
|
177
|
+
Object.defineProperty(target, prop, descriptor);
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
patchSetter(target, prop, onSet) {
|
|
181
|
+
const descriptor = this.lookupDescriptor(target, prop);
|
|
182
|
+
if (!descriptor?.set) return;
|
|
183
|
+
const originalSet = descriptor.set;
|
|
184
|
+
const originalGet = descriptor.get;
|
|
185
|
+
Object.defineProperty(target, prop, {
|
|
186
|
+
...descriptor,
|
|
187
|
+
get: originalGet ? function() {
|
|
188
|
+
return originalGet.call(this);
|
|
189
|
+
} : void 0,
|
|
190
|
+
set(value) {
|
|
191
|
+
onSet();
|
|
192
|
+
originalSet.call(this, value);
|
|
193
|
+
}
|
|
194
|
+
});
|
|
195
|
+
this.restoreFns.push(() => {
|
|
196
|
+
Object.defineProperty(target, prop, descriptor);
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
lookupDescriptor(target, prop) {
|
|
200
|
+
let current = target;
|
|
201
|
+
while (current) {
|
|
202
|
+
const desc = Object.getOwnPropertyDescriptor(current, prop);
|
|
203
|
+
if (desc) return desc;
|
|
204
|
+
current = Object.getPrototypeOf(current);
|
|
205
|
+
}
|
|
206
|
+
return void 0;
|
|
207
|
+
}
|
|
208
|
+
patchWrites() {
|
|
209
|
+
const markDirty = () => {
|
|
210
|
+
this.dirtyFrame = true;
|
|
211
|
+
};
|
|
212
|
+
this.patchMethod(CSSStyleDeclaration.prototype, "setProperty", (orig) => {
|
|
213
|
+
return function(...args) {
|
|
214
|
+
markDirty();
|
|
215
|
+
return orig.apply(this, args);
|
|
216
|
+
};
|
|
217
|
+
});
|
|
218
|
+
this.patchMethod(Element.prototype, "setAttribute", (orig) => {
|
|
219
|
+
return function(...args) {
|
|
220
|
+
const name = args[0]?.toLowerCase?.();
|
|
221
|
+
if (name === "style" || name === "class") markDirty();
|
|
222
|
+
return orig.apply(this, args);
|
|
223
|
+
};
|
|
224
|
+
});
|
|
225
|
+
const classListMethods = ["add", "remove", "toggle", "replace"];
|
|
226
|
+
for (const method of classListMethods) {
|
|
227
|
+
this.patchMethod(DOMTokenList.prototype, method, (orig) => {
|
|
228
|
+
return function(...args) {
|
|
229
|
+
markDirty();
|
|
230
|
+
return orig.apply(this, args);
|
|
231
|
+
};
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
this.patchSetter(HTMLElement.prototype, "className", markDirty);
|
|
235
|
+
const styleDescriptor = this.lookupDescriptor(HTMLElement.prototype, "style");
|
|
236
|
+
if (styleDescriptor?.get) {
|
|
237
|
+
const originalStyleGet = styleDescriptor.get;
|
|
238
|
+
const config = this.config;
|
|
239
|
+
const cache = this.styleProxyCache;
|
|
240
|
+
Object.defineProperty(HTMLElement.prototype, "style", {
|
|
241
|
+
...styleDescriptor,
|
|
242
|
+
get() {
|
|
243
|
+
const real = originalStyleGet.call(this);
|
|
244
|
+
if (!config.enabled) return real;
|
|
245
|
+
const cached = cache.get(this);
|
|
246
|
+
if (cached) return cached;
|
|
247
|
+
const proxy = new Proxy(real, {
|
|
248
|
+
set(target, prop, value) {
|
|
249
|
+
markDirty();
|
|
250
|
+
return Reflect.set(target, prop, value, target);
|
|
251
|
+
}
|
|
252
|
+
});
|
|
253
|
+
cache.set(this, proxy);
|
|
254
|
+
return proxy;
|
|
255
|
+
}
|
|
256
|
+
});
|
|
257
|
+
this.restoreFns.push(() => {
|
|
258
|
+
Object.defineProperty(HTMLElement.prototype, "style", styleDescriptor);
|
|
259
|
+
this.styleProxyCache = /* @__PURE__ */ new WeakMap();
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
patchReads() {
|
|
264
|
+
const onRead = (prop) => this.onLayoutRead(prop);
|
|
265
|
+
const elementReadProps = [
|
|
266
|
+
"offsetWidth",
|
|
267
|
+
"offsetHeight",
|
|
268
|
+
"offsetTop",
|
|
269
|
+
"offsetLeft",
|
|
270
|
+
"offsetParent",
|
|
271
|
+
"clientWidth",
|
|
272
|
+
"clientHeight",
|
|
273
|
+
"clientTop",
|
|
274
|
+
"clientLeft",
|
|
275
|
+
"scrollWidth",
|
|
276
|
+
"scrollHeight",
|
|
277
|
+
"scrollTop",
|
|
278
|
+
"scrollLeft"
|
|
279
|
+
];
|
|
280
|
+
for (const prop of elementReadProps) {
|
|
281
|
+
this.patchGetter(HTMLElement.prototype, prop, onRead);
|
|
282
|
+
}
|
|
283
|
+
this.patchMethod(Element.prototype, "getBoundingClientRect", (orig) => {
|
|
284
|
+
return function() {
|
|
285
|
+
onRead("getBoundingClientRect");
|
|
286
|
+
return orig.call(this);
|
|
287
|
+
};
|
|
288
|
+
});
|
|
289
|
+
this.patchMethod(Element.prototype, "getClientRects", (orig) => {
|
|
290
|
+
return function() {
|
|
291
|
+
onRead("getClientRects");
|
|
292
|
+
return orig.call(this);
|
|
293
|
+
};
|
|
294
|
+
});
|
|
295
|
+
this.patchGetter(HTMLElement.prototype, "innerText", onRead);
|
|
296
|
+
const windowReadProps = ["innerWidth", "innerHeight", "scrollX", "scrollY"];
|
|
297
|
+
for (const prop of windowReadProps) {
|
|
298
|
+
this.patchGetter(window, prop, onRead);
|
|
299
|
+
}
|
|
300
|
+
this.patchMethod(window, "getComputedStyle", (orig) => {
|
|
301
|
+
return function(...args) {
|
|
302
|
+
onRead("getComputedStyle");
|
|
303
|
+
return orig.apply(this, args);
|
|
304
|
+
};
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
};
|
|
308
|
+
var thrash = new Thrash();
|
|
309
|
+
|
|
310
|
+
// packages/core/index.ts
|
|
311
|
+
var VERSION = version;
|
|
312
|
+
|
|
313
|
+
export { Thrash, VERSION, thrash };
|
|
314
|
+
//# sourceMappingURL=index.mjs.map
|
|
315
|
+
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../package.json","../packages/core/core.tsx","../packages/core/index.ts"],"names":[],"mappings":"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","file":"index.mjs","sourcesContent":["{\n \"name\": \"bye-thrash\",\n \"publishConfig\": {\n \"registry\": \"https://registry.npmjs.org\",\n \"access\": \"public\"\n },\n \"version\": \"0.0.1\",\n \"description\": \"Detect and destroy layout thrashing\",\n \"main\": \"dist/index.js\",\n \"module\": \"dist/index.mjs\",\n \"types\": \"dist/index.d.ts\",\n \"files\": [\n \"dist\"\n ],\n \"exports\": {\n \".\": {\n \"types\": \"./dist/index.d.ts\",\n \"default\": \"./dist/index.mjs\"\n }\n },\n \"scripts\": {\n \"build\": \"tsup\",\n \"dev\": \"concurrently \\\"tsup --watch\\\" \\\"cd templates/demo && pnpm dev\\\"\",\n \"typecheck\": \"tsc --noEmit\",\n \"version:package\": \"pnpm changeset version\",\n \"release\": \"pnpm build && pnpm changeset publish\",\n \"lint\": \"eslint -c ./eslint.config.mjs . --fix --no-cache\"\n },\n \"author\": \"joyco.studio\",\n \"license\": \"ISC\",\n \"packageManager\": \"pnpm@10.29.2\",\n \"devDependencies\": {\n \"@changesets/cli\": \"^2.27.11\",\n \"@eslint/js\": \"^9.18.0\",\n \"@types/node\": \"^20.11.24\",\n \"@typescript-eslint/eslint-plugin\": \"^8.21.0\",\n \"@typescript-eslint/parser\": \"^8.21.0\",\n \"eslint\": \"^9.18.0\",\n \"eslint-config-prettier\": \"^10.0.1\",\n \"eslint-plugin-prettier\": \"^5.2.3\",\n \"eslint-plugin-react\": \"^7.37.4\",\n \"eslint-plugin-react-compiler\": \"19.0.0-beta-decd7b8-20250118\",\n \"globals\": \"^15.14.0\",\n \"prettier\": \"^3.4.2\",\n \"tsup\": \"^8.0.2\",\n \"typescript\": \"^5.7.3\",\n \"typescript-eslint\": \"^8.21.0\"\n },\n \"dependencies\": {\n \"concurrently\": \"^9.2.1\"\n }\n}\n","type Mode = 'warn' | 'throw' | 'silent'\n\ninterface ThrashConfig {\n mode: Mode\n ignorePatterns: (string | RegExp)[]\n enabled: boolean\n autoRaf: boolean\n}\n\ninterface Offender {\n count: number\n prop: string\n callSite: string\n stack: string\n lastSeen: number\n}\n\ninterface FrameRead {\n prop: string\n stack: string\n timestamp: number\n}\n\ninterface ReportEntry {\n prop: string\n count: number\n callSite: string\n lastSeen: number\n}\n\ninterface ReportOptions {\n clear?: boolean\n}\n\nconst LIB_FILENAME = 'thrash'\nconst USER_CODE_RE = /\\.(tsx?|jsx?|mjs|cjs)[:)]/\nconst FRAMEWORK_RE =\n /react-dom|react-reconciler|react-server|scheduler|__next|next\\/dist|_next\\/static\\/chunks|webpack|turbopack|hmr-runtime|chunk-/\n\nlet activeInstance: Thrash | null = null\n\nclass Thrash {\n private config: ThrashConfig = {\n mode: 'warn',\n ignorePatterns: [],\n enabled: false,\n autoRaf: true,\n }\n\n private dirtyFrame = false\n private rafId: number | null = null\n private frameReads: FrameRead[] = []\n private offenders = new Map<string, Offender>()\n private styleProxyCache = new WeakMap<HTMLElement, CSSStyleDeclaration>()\n private restoreFns: Array<() => void> = []\n\n configure(options: Partial<ThrashConfig>): void {\n Object.assign(this.config, options)\n }\n\n init(): void {\n if (this.isProduction() && !this.config.enabled) return\n if (this.config.enabled) return\n if (activeInstance && activeInstance !== this) {\n throw new Error('[thrash] Another instance is already active. Call destroy() on it first.')\n }\n\n this.config.enabled = true\n this.dirtyFrame = false\n this.frameReads = []\n\n this.patchWrites()\n this.patchReads()\n // eslint-disable-next-line @typescript-eslint/no-this-alias\n activeInstance = this\n\n if (this.config.autoRaf) {\n this.rafId = requestAnimationFrame(() => this.tick())\n }\n }\n\n destroy(): void {\n this.config.enabled = false\n\n for (const restore of this.restoreFns) {\n try {\n restore()\n } catch {\n // Descriptor may already be gone\n }\n }\n this.restoreFns.length = 0\n\n if (this.rafId !== null) {\n cancelAnimationFrame(this.rafId)\n this.rafId = null\n }\n\n this.offenders.clear()\n this.frameReads = []\n activeInstance = null\n }\n\n tick(): void {\n this.dirtyFrame = false\n this.frameReads = []\n if (this.config.autoRaf) {\n this.rafId = requestAnimationFrame(() => this.tick())\n }\n }\n\n report(options: ReportOptions = {}): ReportEntry[] {\n const { clear = true } = options\n\n const entries: ReportEntry[] = []\n for (const [, offender] of this.offenders) {\n entries.push({\n prop: offender.prop,\n count: offender.count,\n callSite: offender.callSite,\n lastSeen: offender.lastSeen,\n })\n }\n\n entries.sort((a, b) => b.count - a.count)\n\n if (entries.length > 0) {\n console.table(entries)\n } else {\n console.log('[thrash] No layout thrashing detected.')\n }\n\n if (clear) this.offenders.clear()\n\n return entries\n }\n\n // -------------------------------------------------------------------------\n // Detection\n // -------------------------------------------------------------------------\n\n private onLayoutRead(prop: string): void {\n if (!this.config.enabled) return\n\n const stack = new Error().stack ?? ''\n\n if (this.shouldIgnore(stack)) return\n\n this.frameReads.push({ prop, stack, timestamp: performance.now() })\n\n if (!this.dirtyFrame) return\n\n const callSite = this.extractCallSite(stack)\n const key = `${prop}::${callSite}`\n const existing = this.offenders.get(key)\n\n if (existing) {\n existing.count++\n existing.lastSeen = Date.now()\n } else {\n this.offenders.set(key, {\n count: 1,\n prop,\n callSite,\n stack,\n lastSeen: Date.now(),\n })\n }\n\n const message = `[thrash] Layout read \"${prop}\" after style write. Call site: ${callSite}`\n\n if (this.config.mode === 'throw') {\n throw new Error(message)\n } else if (this.config.mode === 'warn') {\n console.warn(message)\n }\n }\n\n private extractCallSite(stack: string): string {\n const lines = stack.split('\\n').slice(1)\n\n for (const line of lines) {\n const trimmed = line.trim()\n if (trimmed.includes(LIB_FILENAME)) continue\n if (trimmed.includes('node_modules')) continue\n if (FRAMEWORK_RE.test(trimmed)) continue\n if (USER_CODE_RE.test(trimmed)) {\n return trimmed.replace(/^\\s*at\\s+/, '')\n }\n }\n\n for (const line of lines) {\n const trimmed = line.trim()\n if (trimmed.includes(LIB_FILENAME)) continue\n if (trimmed.includes('node_modules')) continue\n if (FRAMEWORK_RE.test(trimmed)) continue\n return trimmed.replace(/^\\s*at\\s+/, '')\n }\n\n for (const line of lines) {\n const trimmed = line.trim()\n if (trimmed.includes(LIB_FILENAME)) continue\n return trimmed.replace(/^\\s*at\\s+/, '')\n }\n\n return 'unknown'\n }\n\n private shouldIgnore(stack: string): boolean {\n return this.config.ignorePatterns.some((pattern) => {\n if (typeof pattern === 'string') return stack.includes(pattern)\n return pattern.test(stack)\n })\n }\n\n private isProduction(): boolean {\n try {\n return process.env.NODE_ENV === 'production'\n } catch {\n return false\n }\n }\n\n private patchMethod<T extends object>(\n target: T,\n methodName: string,\n wrapper: (original: (...args: unknown[]) => unknown) => (...args: unknown[]) => unknown\n ): void {\n const original = (target as Record<string, unknown>)[methodName] as (...args: unknown[]) => unknown\n if (typeof original !== 'function') return\n\n const patched = wrapper(original)\n ;(target as Record<string, unknown>)[methodName] = patched\n\n this.restoreFns.push(() => {\n ;(target as Record<string, unknown>)[methodName] = original\n })\n }\n\n private patchGetter<T extends object>(target: T, prop: string, onGet: (prop: string) => void): void {\n const descriptor = this.lookupDescriptor(target, prop)\n if (!descriptor?.get) return\n\n const originalGet = descriptor.get\n\n Object.defineProperty(target, prop, {\n ...descriptor,\n get() {\n onGet(prop)\n return originalGet.call(this)\n },\n })\n\n this.restoreFns.push(() => {\n Object.defineProperty(target, prop, descriptor)\n })\n }\n\n private patchSetter<T extends object>(target: T, prop: string, onSet: () => void): void {\n const descriptor = this.lookupDescriptor(target, prop)\n if (!descriptor?.set) return\n\n const originalSet = descriptor.set\n const originalGet = descriptor.get\n\n Object.defineProperty(target, prop, {\n ...descriptor,\n get: originalGet\n ? function (this: unknown) {\n return originalGet.call(this)\n }\n : undefined,\n set(value: unknown) {\n onSet()\n originalSet.call(this, value)\n },\n })\n\n this.restoreFns.push(() => {\n Object.defineProperty(target, prop, descriptor)\n })\n }\n\n private lookupDescriptor(target: object, prop: string): PropertyDescriptor | undefined {\n let current: object | null = target\n while (current) {\n const desc = Object.getOwnPropertyDescriptor(current, prop)\n if (desc) return desc\n current = Object.getPrototypeOf(current) as object | null\n }\n return undefined\n }\n\n private patchWrites(): void {\n const markDirty = () => {\n this.dirtyFrame = true\n }\n\n this.patchMethod(CSSStyleDeclaration.prototype, 'setProperty', (orig) => {\n return function (this: CSSStyleDeclaration, ...args: unknown[]) {\n markDirty()\n return orig.apply(this, args)\n }\n })\n\n this.patchMethod(Element.prototype, 'setAttribute', (orig) => {\n return function (this: Element, ...args: unknown[]) {\n const name = (args[0] as string)?.toLowerCase?.()\n if (name === 'style' || name === 'class') markDirty()\n return orig.apply(this, args)\n }\n })\n\n const classListMethods = ['add', 'remove', 'toggle', 'replace'] as const\n for (const method of classListMethods) {\n this.patchMethod(DOMTokenList.prototype, method, (orig) => {\n return function (this: DOMTokenList, ...args: unknown[]) {\n markDirty()\n return orig.apply(this, args)\n }\n })\n }\n\n this.patchSetter(HTMLElement.prototype, 'className', markDirty)\n\n // Proxy the `style` getter so any property assignment on the returned\n // CSSStyleDeclaration marks the frame dirty. Browsers implement CSS\n // properties as exotic/internal properties without standard descriptors,\n // so patching individual setters doesn't work.\n const styleDescriptor = this.lookupDescriptor(HTMLElement.prototype, 'style')\n if (styleDescriptor?.get) {\n const originalStyleGet = styleDescriptor.get\n const config = this.config\n const cache = this.styleProxyCache\n\n Object.defineProperty(HTMLElement.prototype, 'style', {\n ...styleDescriptor,\n get(this: HTMLElement) {\n const real = originalStyleGet.call(this) as CSSStyleDeclaration\n if (!config.enabled) return real\n\n const cached = cache.get(this)\n if (cached) return cached\n\n const proxy = new Proxy(real, {\n set(target, prop, value) {\n markDirty()\n return Reflect.set(target, prop, value, target)\n },\n })\n\n cache.set(this, proxy)\n return proxy\n },\n })\n\n this.restoreFns.push(() => {\n Object.defineProperty(HTMLElement.prototype, 'style', styleDescriptor)\n this.styleProxyCache = new WeakMap()\n })\n }\n }\n\n private patchReads(): void {\n const onRead = (prop: string) => this.onLayoutRead(prop)\n\n const elementReadProps = [\n 'offsetWidth',\n 'offsetHeight',\n 'offsetTop',\n 'offsetLeft',\n 'offsetParent',\n 'clientWidth',\n 'clientHeight',\n 'clientTop',\n 'clientLeft',\n 'scrollWidth',\n 'scrollHeight',\n 'scrollTop',\n 'scrollLeft',\n ]\n\n for (const prop of elementReadProps) {\n this.patchGetter(HTMLElement.prototype, prop, onRead)\n }\n\n this.patchMethod(Element.prototype, 'getBoundingClientRect', (orig) => {\n return function (this: Element) {\n onRead('getBoundingClientRect')\n return orig.call(this)\n }\n })\n\n this.patchMethod(Element.prototype, 'getClientRects', (orig) => {\n return function (this: Element) {\n onRead('getClientRects')\n return orig.call(this)\n }\n })\n\n this.patchGetter(HTMLElement.prototype, 'innerText', onRead)\n\n const windowReadProps = ['innerWidth', 'innerHeight', 'scrollX', 'scrollY']\n for (const prop of windowReadProps) {\n this.patchGetter(window, prop, onRead)\n }\n\n this.patchMethod(window, 'getComputedStyle', (orig) => {\n return function (this: typeof window, ...args: unknown[]) {\n onRead('getComputedStyle')\n return orig.apply(this, args)\n }\n })\n }\n}\n\nexport const thrash = new Thrash()\nexport { Thrash }\nexport type { ThrashConfig, ReportEntry, ReportOptions }\n","import { version } from '../../package.json'\n\nexport const VERSION = version\nexport * from './core'\n"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "bye-thrash",
|
|
3
|
+
"publishConfig": {
|
|
4
|
+
"registry": "https://registry.npmjs.org",
|
|
5
|
+
"access": "public"
|
|
6
|
+
},
|
|
7
|
+
"version": "0.0.1",
|
|
8
|
+
"description": "Detect and destroy layout thrashing",
|
|
9
|
+
"main": "dist/index.js",
|
|
10
|
+
"module": "dist/index.mjs",
|
|
11
|
+
"types": "dist/index.d.ts",
|
|
12
|
+
"files": [
|
|
13
|
+
"dist"
|
|
14
|
+
],
|
|
15
|
+
"exports": {
|
|
16
|
+
".": {
|
|
17
|
+
"types": "./dist/index.d.ts",
|
|
18
|
+
"default": "./dist/index.mjs"
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
"author": "joyco.studio",
|
|
22
|
+
"license": "ISC",
|
|
23
|
+
"devDependencies": {
|
|
24
|
+
"@changesets/cli": "^2.27.11",
|
|
25
|
+
"@eslint/js": "^9.18.0",
|
|
26
|
+
"@types/node": "^20.11.24",
|
|
27
|
+
"@typescript-eslint/eslint-plugin": "^8.21.0",
|
|
28
|
+
"@typescript-eslint/parser": "^8.21.0",
|
|
29
|
+
"eslint": "^9.18.0",
|
|
30
|
+
"eslint-config-prettier": "^10.0.1",
|
|
31
|
+
"eslint-plugin-prettier": "^5.2.3",
|
|
32
|
+
"eslint-plugin-react": "^7.37.4",
|
|
33
|
+
"eslint-plugin-react-compiler": "19.0.0-beta-decd7b8-20250118",
|
|
34
|
+
"globals": "^15.14.0",
|
|
35
|
+
"prettier": "^3.4.2",
|
|
36
|
+
"tsup": "^8.0.2",
|
|
37
|
+
"typescript": "^5.7.3",
|
|
38
|
+
"typescript-eslint": "^8.21.0"
|
|
39
|
+
},
|
|
40
|
+
"dependencies": {
|
|
41
|
+
"concurrently": "^9.2.1"
|
|
42
|
+
},
|
|
43
|
+
"scripts": {
|
|
44
|
+
"build": "tsup",
|
|
45
|
+
"dev": "concurrently \"tsup --watch\" \"cd templates/demo && pnpm dev\"",
|
|
46
|
+
"typecheck": "tsc --noEmit",
|
|
47
|
+
"version:package": "pnpm changeset version",
|
|
48
|
+
"release": "pnpm build && pnpm changeset publish",
|
|
49
|
+
"lint": "eslint -c ./eslint.config.mjs . --fix --no-cache"
|
|
50
|
+
}
|
|
51
|
+
}
|