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

@firsttx/prepaint

v0.9.0

Published

Instant boot script for CSR apps that replays IndexedDB snapshots before React loads, delivering ~0 ms revisit blanks with automatic hydration or clean rerender fallback.

Downloads

964

Readme

@firsttx/prepaint

Instant replay for CSR apps — ~0ms blank screen on revisit

Restores your app's last visual state from IndexedDB before JavaScript loads. No blank screens. Automatic React hydration with graceful fallback.

npm install @firsttx/prepaint

npm version License


Why Prepaint?

The only solution that restores UI before JavaScript loads.

  • No SSR/SSG infrastructure needed
  • Works with any existing CSR React app
  • Automatic React hydration with graceful fallback
  • Native ViewTransition support

The Problem

Traditional CSR on revisit:
User clicks → Blank screen (2000ms) → Content appears

With Prepaint:
User clicks → Last snapshot (~0ms) → React hydrates → Fresh data

Prepaint captures DOM snapshots per route and replays them instantly on the next visit.


Quick Start

1. Vite Plugin

// vite.config.ts
import { firstTx } from '@firsttx/prepaint/plugin/vite';

export default defineConfig({
  plugins: [firstTx()],
});

2. React Entry

// main.tsx
import { createFirstTxRoot } from '@firsttx/prepaint';

createFirstTxRoot(document.getElementById('root')!, <App />);

Done. Prepaint now:

  • Captures snapshots on page hide/unload
  • Restores them in ~0ms on revisit
  • Hydrates with React (or falls back gracefully)

How It Works

Three Phases

┌─────────────────────────────────┐
│ 1) Capture (on page leave)     │
│  - beforeunload/pagehide/       │
│    visibilitychange             │
│  - Saves DOM + styles to        │
│    IndexedDB                    │
└─────────────────────────────────┘
              ↓
┌─────────────────────────────────┐
│ 2) Boot (~0ms on revisit)       │
│  - Inline script runs           │
│  - Reads snapshot from IndexedDB│
│  - Injects into #root           │
└─────────────────────────────────┘
              ↓
┌─────────────────────────────────┐
│ 3) Handoff (~500ms)             │
│  - Main bundle loads            │
│  - Hydrates or client-renders   │
│  - Cleans up prepaint artifacts │
└─────────────────────────────────┘

Storage

  • DB: firsttx-prepaint
  • Store: snapshots
  • Key: route pathname
  • TTL: 7 days

API

createFirstTxRoot(container, element, options?)

createFirstTxRoot(
  container: HTMLElement,
  element: ReactElement,
  options?: {
    transition?: boolean;  // ViewTransition (default: true)
    onCapture?: (snapshot: Snapshot) => void;
    onHandoff?: (strategy: 'has-prepaint' | 'cold-start') => void;
    onHydrationError?: (error: Error) => void;
  }
);

Behavior

  • If snapshot exists + #root has 1 child → hydrateRoot()
  • Otherwise → createRoot() fresh render
  • On hydration error → fallback to clean render (with ViewTransition)

firstTx(options?) (Vite Plugin)

firstTx({
  inline?: boolean,              // Inline boot script (default: true)
  minify?: boolean,              // Minify boot script (default: !isDev)
  injectTo?: 'head-prepend' | 'head' | 'body-prepend' | 'body',
  nonce?: string | (() => string),
  overlay?: boolean,             // Enable overlay mode globally
  overlayRoutes?: string[],      // Overlay for specific routes
})

Overlay Mode

Problem Direct injection into #root can race with routers, causing duplicate DOM.

Solution Overlay mode paints the snapshot above your app in Shadow DOM, then fades out after hydration.

Enable Overlay

// Option 1: Global flag
window.__FIRSTTX_OVERLAY__ = true;

// Option 2: localStorage (persists)
localStorage.setItem('firsttx:overlay', '1');

// Option 3: Specific routes
localStorage.setItem('firsttx:overlayRoutes', '/prepaint,/dashboard');

// Option 4: Vite plugin
firstTx({ overlay: true });

Disable

delete window.__FIRSTTX_OVERLAY__;
localStorage.removeItem('firsttx:overlay');
localStorage.removeItem('firsttx:overlayRoutes');

Real-World Patterns

With Local-First

import { useModel } from '@firsttx/local-first';

function ProductsPage() {
  const [products] = useModel(ProductsModel);

  // Prepaint shows last snapshot
  // useModel provides instant data from IndexedDB
  if (!products) return <Skeleton />;

  return <ProductList products={products} />;
}

Mark Volatile Content

// These change on every render → exclude from snapshot
<span data-firsttx-volatile>{Date.now()}</span>
<div data-firsttx-volatile>{Math.random()}</div>
<Timer data-firsttx-volatile />

Debug Lifecycle

createFirstTxRoot(root, <App />, {
  onCapture: (snapshot) => console.log('Captured:', snapshot.route),
  onHandoff: (strategy) => console.log('Strategy:', strategy),
  onHydrationError: (err) => console.error('Hydration failed:', err),
});

Hydration & Fallback

Single-Child Rule

Prepaint only attempts hydration if #root has exactly 1 child. Otherwise → fresh render.

Root Guard

After mount, a MutationObserver watches #root:

  • Detects extra children (e.g., router appending siblings)
  • Unmounts → clears → re-renders cleanly
  • Prevents "double UI" issues

Cleanup

Post-mount:

  • Removes <html data-prepaint="true">
  • Removes style[data-firsttx-prepaint]
  • Removes overlay host #__firsttx_prepaint__

Best Practices

DO

Use ViewTransition (default)

createFirstTxRoot(root, <App />, { transition: true });

Mark volatile content

<span data-firsttx-volatile>{timestamp}</span>

Combine with Local-First

const [data] = useModel(Model);
// Instant data from IndexedDB while network refreshes

DON'T

Don't expect instant availability on first visit

// First visit: snapshot doesn't exist yet
// Only kicks in on second+ visits

Don't capture sensitive data

// Snapshots are plain HTML in IndexedDB
// Avoid capturing auth tokens, PII, etc.

Debugging

Development Logs

[FirstTx] Snapshot restored (age: 63ms)
[FirstTx] Prepaint detected (age: 63ms)
[FirstTx] Snapshot captured for /products

Inspect IndexedDB

indexedDB.open('firsttx-prepaint').onsuccess = (e) => {
  const db = e.target.result;
  const tx = db.transaction('snapshots', 'readonly');
  tx.objectStore('snapshots').getAll().onsuccess = (e2) => {
    console.log(e2.target.result);
  };
};

Check Injection

On revisit, look for:

  • <html data-prepaint="true" data-prepaint-timestamp="...">
  • Boot script near <head> top
  • Temporary style[data-firsttx-prepaint]

Performance

| Metric | Target | Actual | | ----------------- | ------ | ------- | | Boot script size | <2KB | ~1.7KB | | Boot execution | <20ms | ~15ms | | Hydration success | >80% | ~80-85% |

Hydration mismatches (timestamps, random IDs, client-only branches) automatically fallback to clean render with ViewTransition.


Browser Support

| Browser | Min Version | ViewTransition | Status | | ----------- | ---------------------------- | -------------- | ------------- | | Chrome/Edge | 111+ | ✅ Full | ✅ Tested | | Firefox | Latest | ❌ No | ✅ Fallback | | Safari | 16+ | ❌ No | ✅ Fallback | | Mobile | iOS 16+, Android Chrome 111+ | Varies | ✅ Core works |


Limitations

| Issue | Workaround | | ---------------------- | ------------------------------------ | | Vite-only plugin | Manual <script> for other bundlers | | Fixed 7-day TTL | Override in source (config planned) | | Full-page capture only | Sub-tree snapshots not supported yet |


FAQ

Q: Does this work with SSR/Next.js?

A: No. Prepaint targets pure CSR apps. For SSR, use framework's native features.


Q: Will this increase memory usage?

A: Snapshots live in IndexedDB (disk). Memory overhead is minimal during boot.


Q: How do I prevent duplicate UI?

A: Use overlay mode or rely on the root guard:

firstTx({ overlay: true });

Q: Can I restrict capture to certain routes?

A: Use setupCapture() manually or filter at app layer. Default captures all routes.


Q: What if hydration fails?

A: Automatic fallback to clean client render (with ViewTransition if enabled). No manual intervention needed.


Changelog

0.3.0 - 2025.10.12

Add overlay mode and hard hydration bailout to prepaint

This release introduces significant improvements to snapshot capture and hydration reliability:

Breaking Changes

  • captureSnapshot() now uses root element serialization instead of body.innerHTML
  • Requires #root element to be present for capture to work

New Features

  • Overlay mode support Adds global __FIRSTTX_DEV__ flag for development logging
  • Volatile data handling Elements with data-firsttx-volatile attribute are automatically cleared during capture (useful for timestamps, random values)
  • Style filtering Prepaint-injected styles are now excluded from capture via data-firsttx-prepaint attribute
  • Enhanced capture timing
    • Captures on visibilitychange (when page becomes hidden)
    • Captures on pagehide (mobile-friendly)
    • Maintains beforeunload capture
    • Debounced with microtask queue to prevent duplicate saves

Improvements

  • Cleaner serialization: Only captures first child of root element
  • More reliable hydration: Filters out dynamic content that causes mismatches
  • Better mobile support: pagehide event works more reliably on iOS/Android
  • Development experience: __FIRSTTX_DEV__ replaces process.env.NODE_ENV checks

Migration Guide

// If you have dynamic content that changes on every render:
<span data-firsttx-volatile>{Date.now()}</span>
<div data-firsttx-volatile>{Math.random()}</div>

// Vite plugin automatically injects __FIRSTTX_DEV__ flag
// No changes needed to your vite.config.ts

Related Packages


License

MIT © joseph0926