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

react-tab-refresh

v1.0.0

Published

Stop the Memory Bloat — Automatically prune and re-hydrate your long-lived React apps to keep the browser fast.

Readme

react-tab-refresh

Stop the Memory Bloat — Automatically prune and re-hydrate your long-lived React apps to keep the browser fast.

npm version Bundle Size License: MIT


📋 The Problem

In 2026, users keep tabs open for weeks. Even with optimized code, DOM nodes, event listeners, and JS heaps grow over time. This leads to:

  • Tab Crashing: Browser kills the process due to high memory usage
  • System Slowdown: Your app slows down the user's entire OS
  • Stale Data: Background tabs show data from 3 days ago

✨ The Solution

react-tab-refresh monitors your app's health. When a tab is hidden and exceeds memory limits or inactivity timers:

  1. Serializes your essential state to sessionStorage
  2. Unmounts the entire heavy React tree (freeing the heap)
  3. Re-mounts and restores everything instantly when the user returns

🚀 Quick Start

Installation

npm install react-tab-refresh
# or
yarn add react-tab-refresh
# or
pnpm add react-tab-refresh

1. Wrap Your App

import { PruneProvider } from 'react-tab-refresh';

function App() {
  return (
    <PruneProvider config={{ pruneAfter: '30m' }}>
      <MainDashboard />
    </PruneProvider>
  );
}

2. Mark "Essential" State

Replace useState with usePrunableState for data that must survive the pruning process.

import { usePrunableState } from 'react-tab-refresh';

function SearchComponent() {
  // This state will be saved to sessionStorage before the component is unmounted
  // and restored automatically upon return.
  const [results, setResults] = usePrunableState('search_results', []);
  const [query, setQuery] = usePrunableState('search_query', '');

  return (
    <div>
      <input
        value={query}
        onChange={(e) => setQuery(e.target.value)}
        placeholder="Search..."
      />
      {results.map((result) => (
        <div key={result.id}>{result.title}</div>
      ))}
    </div>
  );
}

That's it! Your app will now automatically prune and rehydrate.


📚 API Reference

<PruneProvider>

The main provider component that wraps your app.

<PruneProvider
  config={{
    pruneAfter: '30m',              // When to prune (default: 30 minutes)
    maxMemoryMb: 600,               // Memory threshold (Chrome only)
    enableMemoryMonitoring: false,  // Enable memory-based pruning
    maxDomNodes: 10000,             // DOM node threshold
    onPrune: () => {},              // Callback before pruning
    onRehydrate: () => {},          // Callback after rehydration
    debug: false,                   // Enable debug logging
  }}
  placeholder={<LoadingScreen />}   // Show during rehydration
>
  <App />
</PruneProvider>

Config Options

| Option | Type | Default | Description | |--------|------|---------|-------------| | pruneAfter | string \| number | '30m' | Inactivity time before pruning. Supports: '30m', '1h', '2d', or milliseconds | | maxMemoryMb | number | undefined | Memory threshold in MB (Chrome only) | | enableMemoryMonitoring | boolean | false | Enable memory-based pruning | | maxDomNodes | number | undefined | Maximum DOM nodes before pruning | | onPrune | () => void \| Promise<void> | undefined | Callback before pruning (for cleanup) | | onRehydrate | () => void \| Promise<void> | undefined | Callback after rehydration (for reconnection) | | debug | boolean | false | Enable debug logging |

usePrunableState()

Drop-in replacement for useState with automatic persistence.

const [state, setState] = usePrunableState(key, initialValue, options);

Parameters

| Parameter | Type | Description | |-----------|------|-------------| | key | string | Unique storage key | | initialValue | T | Initial value (used if no stored value) | | options | PrunableStateOptions<T> | Optional configuration |

Options

interface PrunableStateOptions<T> {
  serialize?: (value: T) => string;        // Custom serializer
  deserialize?: (value: string) => T;      // Custom deserializer
  validate?: (value: T) => boolean;        // Validate restored data
  ttl?: number;                            // Time-to-live in ms
  onExpired?: () => void;                  // Callback when data expires
  debug?: boolean;                         // Enable debug logging
}

Example with Options

const [user, setUser] = usePrunableState(
  'current_user',
  { id: null, name: '' },
  {
    validate: (value) => value.id !== null,
    ttl: 24 * 60 * 60 * 1000, // 24 hours
    onExpired: () => fetchFreshUserData(),
  }
);

usePruningState()

Access pruning state and utilities.

const {
  isPruned,           // Whether app is currently pruned
  isRehydrating,      // Whether app is rehydrating
  metrics,            // Current metrics (inactiveMs, memoryMb, etc.)
  forceRehydrate,     // Force immediate rehydration
  registerCleanup,    // Register cleanup function
  unregisterCleanup,  // Unregister cleanup function
} = usePruningState();

Example: WebSocket Cleanup

import { usePruningState } from 'react-tab-refresh';

function ChatComponent() {
  const { registerCleanup } = usePruningState();

  useEffect(() => {
    const ws = new WebSocket('wss://api.example.com/chat');

    // Register cleanup to close WebSocket before pruning
    registerCleanup('websocket', () => {
      ws.close();
    });

    return () => ws.close();
  }, [registerCleanup]);

  return <div>Chat UI</div>;
}

⚠️ Common Issues & Solutions

Issue: State Loss

Cause: Non-serializable data (Functions, Classes) in state.

Solution: Use the transform option or avoid storing non-serializable data.

// ❌ Bad: Functions can't be serialized
const [handler, setHandler] = usePrunableState('handler', () => {});

// ✅ Good: Store serializable data only
const [config, setConfig] = usePrunableState('config', { url: '/api' });

Issue: Flicker on Return

Cause: Re-mounting large trees takes time.

Solution: Use the placeholder prop to show a skeleton screen.

<PruneProvider
  config={{ pruneAfter: '30m' }}
  placeholder={<SkeletonScreen />}
>
  <App />
</PruneProvider>

Issue: WebSocket Drop

Cause: Unmounting closes active connections.

Solution: Use registerCleanup to gracefully close and onRehydrate to reconnect.

const { registerCleanup } = usePruningState();

useEffect(() => {
  const ws = new WebSocket(url);

  registerCleanup('websocket', () => {
    ws.close();
  });

  return () => ws.close();
}, []);

Issue: Quota Exceeded

Cause: SessionStorage has a 5-10MB limit.

Solution: Reduce state size or use selective persistence.

// Only persist essential data
const [largeData, setLargeData] = useState([]); // Not persisted
const [essentialData, setEssentialData] = usePrunableState('essential', {}); // Persisted

🎯 Advanced Usage

Custom Serialization

For complex data types (Dates, Maps, Sets):

const [timestamp, setTimestamp] = usePrunableState(
  'timestamp',
  new Date(),
  {
    serialize: (date) => date.toISOString(),
    deserialize: (str) => new Date(str),
  }
);

Conditional Pruning

const { isPruned, forceRehydrate } = usePruningState();

if (isPruned && userClickedButton) {
  forceRehydrate();
}

Monitoring Metrics

const { metrics } = usePruningState();

console.log(`Inactive for: ${metrics.inactiveMs}ms`);
console.log(`Memory usage: ${metrics.memoryMb}MB`);
console.log(`DOM nodes: ${metrics.domNodes}`);

🧪 Testing

The package includes comprehensive tests. Run them with:

npm test

For coverage:

npm run test:coverage

📦 Bundle Size

react-tab-refresh is designed to be lightweight:

  • Minified: ~8KB
  • Gzipped: ~3KB

🌐 Browser Support

| Feature | Chrome | Firefox | Safari | Edge | |---------|--------|---------|--------|------| | Page Visibility API | ✅ | ✅ | ✅ | ✅ | | SessionStorage | ✅ | ✅ | ✅ | ✅ | | Memory Monitoring | ✅ | ❌ | ❌ | ✅ |

Note: Memory monitoring (performance.memory) is Chrome/Edge only. The package gracefully degrades to time-based pruning on other browsers.


🤝 Contributing

Contributions are welcome! Please read our Contributing Guide for details.


📄 License

MIT © TAIJULAMAN


🙏 Acknowledgments

Inspired by the real-world problem of tab bloat in modern web applications. Built for the 2026 web where apps live in tabs for weeks.