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 ADDED
@@ -0,0 +1,205 @@
1
+ # <img src="./static/JOYCO.png" alt="JOYCO Logo" height="36" width="36" align="top" />&nbsp;&nbsp;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
+ ```
@@ -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 };
@@ -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
+ }