npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

bye-thrash

v0.1.3

Published

Detect and destroy layout thrashing

Readme

  JOYCO | Bye Thrash

Detect and destroy layout thrashing

A dev-only library that patches browser APIs to detect layout-triggering reads after style mutations in the same animation frame.

What is layout thrashing?

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.

// Bad, triggers forced reflow
el.style.width = '100px'
const w = el.offsetWidth // browser must recalculate layout NOW

// Worse, N forced reflows in a loop
for (const box of boxes) {
  box.style.width = box.offsetWidth + 10 + 'px'
}

Installation

npm install bye-thrash

Quick Start

'use client'

import { useEffect } from 'react'
import { thrash } from 'bye-thrash'

export function ThrashMonitor() {
  useEffect(() => {
    thrash.init()
    return () => thrash.destroy()
  }, [])

  return null
}

Drop <ThrashMonitor /> into your app layout. Open DevTools console, any layout thrashing will log warnings like:

[thrash] Layout read "offsetWidth" after style write. Call site: handleResize (src/components/Grid.tsx:42:8)

API

thrash.init()

Patches browser APIs and begins detecting layout thrashing. Automatically skipped in production (NODE_ENV === 'production') unless explicitly enabled via configure({ enabled: true }).

thrash.destroy()

Restores all patched APIs to their originals, cancels the internal frame loop, and clears all recorded data. Safe to call multiple times.

thrash.configure(options)

Configure behavior before or after calling init().

thrash.configure({
  mode: 'warn',           // 'warn' | 'throw' | 'silent'
  ignorePatterns: [],      // (string | RegExp)[] — stack traces matching these are ignored
  autoRaf: true,           // whether bye-thrash manages its own requestAnimationFrame loop
  scope: null,             // Element | null — restrict detection to a DOM subtree
})

| Option | Default | Description | |--------|---------|-------------| | mode | 'warn' | 'warn' logs to console, 'throw' throws an error, 'silent' records without output | | ignorePatterns | [] | Stack trace patterns to ignore, useful for known third-party thrashing you can't fix | | 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) | | scope | null | When set to a DOM element, only layout reads on elements inside that subtree are tracked. Reads outside the scope are completely ignored. When null, all reads are tracked |

thrash.tick()

Manually resets the frame state (dirtyFrame and per-frame read tracking). Only needed when autoRaf is false.

thrash.report(options?)

Returns an array of ReportEntry objects for all detected thrashing, sorted by count (highest first). Also logs a console.table (unless mode is 'silent').

const entries = thrash.report()
// [{ prop: 'offsetWidth', count: 12, callSite: '...', lastSeen: 1711600000000 }]

// Keep accumulated data (don't clear after reporting)
thrash.report({ clear: false })

Analyze reports with Claude Code

If you use Claude Code, the thrash-report-analyzer skill can parse your report, trace each stack back to the offending source lines, and suggest fixes automatically.

Install the skill, then paste your report array and run /thrash-report-analyzer:

/thrash-report-analyzer

Scope

By default bye-thrash monitors the entire page. If you only care about thrashing inside a specific part of the DOM, set scope to a container element — any layout read outside that subtree will be silently ignored.

thrash.configure({
  scope: document.getElementById('main-content'),
})
thrash.init()

Under the hood, scope uses Element.contains() to check whether the element being read is a descendant of the scope boundary. The filtering happens early in the detection pipeline, so out-of-scope reads incur no tracking overhead.

Behavior details

| Scenario | Result | |----------|--------| | No scope set (null) | All layout reads are tracked | | Scope set, read on element inside scope | Tracked normally | | Scope set, read on element outside scope | Ignored | | Scope set, read has no element (e.g. window.innerWidth) | Ignored |

React example

import { useRef, useEffect } from 'react'
import { thrash } from 'bye-thrash'

export function ScopedThrashMonitor({ children }: { children: React.ReactNode }) {
  const ref = useRef<HTMLDivElement>(null)

  useEffect(() => {
    thrash.configure({ scope: ref.current })
    thrash.init()
    return () => thrash.destroy()
  }, [])

  return <div ref={ref}>{children}</div>
}

Wrap the section you want to monitor — thrashing from third-party widgets, browser extensions, or unrelated parts of your app won't show up in the report.

What gets detected

Writes (mark frame as dirty)

| API | Example | |-----|---------| | Any el.style.* assignment | el.style.width = '100px' | | style.setProperty() | el.style.setProperty('--x', '10') | | setAttribute for style / class | el.setAttribute('class', 'foo') | | classList.* | el.classList.add('active') | | className | el.className = 'foo bar' |

Reads (trigger warning if frame is dirty)

| API | Example | |-----|---------| | offsetWidth/Height/Top/Left | el.offsetWidth | | clientWidth/Height/Top/Left | el.clientHeight | | scrollWidth/Height/Top/Left | el.scrollTop | | getBoundingClientRect() | el.getBoundingClientRect() | | getClientRects() | el.getClientRects() | | getComputedStyle() | window.getComputedStyle(el) | | innerText | el.innerText | | window.innerWidth/Height | window.innerWidth | | window.scrollX/Y | window.scrollY |

Manual tick

If you use a library like 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.

Disable the internal loop and call tick() yourself at the start of each frame:

import { thrash } from 'bye-thrash'
import Tempus from 'tempus'

thrash.configure({ autoRaf: false })
thrash.init()

// Reset frame state before anything else runs in the frame.
// Use a very low priority so tick() executes first.
const unsub = Tempus.add(() => {
  thrash.tick()
}, { priority: -100 })

Why this matters

Tempus merges every requestAnimationFrame call in your app into a single orchestrated loop, executing callbacks in priority order (lowest first). When bye-thrash manages its own rAF (autoRaf: true), its tick() becomes just another unordered callback, it may run after your style writes have already happened, so the frame is never properly reset.

By calling tick() inside a Tempus callback with a very low priority (e.g. -100), you guarantee it runs before any of your application code, giving you an accurate clean/dirty frame boundary.

Priority guide

| Priority | Role | |----------|------| | -100 | thrash.tick(), reset frame state | | 0 (default) | Your application code (animations, style writes, layout reads) | | 100+ | Post-frame work (metrics, logging) |

Cleanup

The Tempus.add() call returns an unsubscribe function. Call it alongside thrash.destroy() when tearing down:

// React example
import { useEffect } from 'react'
import { thrash } from 'bye-thrash'
import Tempus from 'tempus'

export function ThrashMonitor() {
  useEffect(() => {
    thrash.configure({ autoRaf: false })
    thrash.init()

    const unsub = Tempus.add(() => {
      thrash.tick()
    }, { priority: -100 })

    return () => {
      unsub()
      thrash.destroy()
    }
  }, [])

  return null
}

How to fix thrashing

Read first, write second:

const w = el.offsetWidth       // read (clean frame)
el.style.width = w + 50 + 'px' // write

Batch reads and writes separately:

// All reads
const widths = boxes.map(b => b.offsetWidth)

// All writes
boxes.forEach((b, i) => {
  b.style.width = widths[i] + 10 + 'px'
})

Defer reads to the next frame:

el.style.width = '200px'
requestAnimationFrame(() => {
  const rect = el.getBoundingClientRect() // safe, new frame
})

Types

import type { ThrashConfig, ReportEntry, ReportOptions } from 'bye-thrash'

Version Management

This library uses Changesets to manage versions and publish releases.

Adding a changeset

When you make changes that need to be released:

pnpm changeset

This will prompt you to:

  1. Select which packages you want to include in the changeset
  2. Choose whether it's a major/minor/patch bump
  3. Provide a summary of the changes

Creating a release

# 1. Create new versions of packages
pnpm version:package

# 2. Release (builds and publishes to npm)
pnpm release