logo
Firsttx
No more blank CSR revisits

Stop blank screens on CSR revisits.A three-layer toolkit that restores the last screen instantly.

FirstTx combines Prepaint, Local-First, and Tx into one toolkit. Keep your CSR architecture while adding instant revisit recovery, offline resilience, and safe optimistic UI.

Internal tools / dashboardsOffline-first appsLarge CSR apps
Revisit timeline
FirstTx DevTools

Before FirstTx

User revisit

User clicks back or re-enters the CSR app.

Blank screen

1-2 seconds of white screen or spinner while JS and data load.

JS load & mount

React mounts only after bundles and API calls finish.

After FirstTx

Boot snippet

A tiny boot script runs right after the HTML is loaded.

Last screen restore

Restores the last DOM snapshot from IndexedDB instantly on revisit.

Hydration & sync

React hydrates and data sync runs in the background while the user continues.

With the View Transition API, restores can become smooth crossfades instead of hard cuts.
Revisits0 ms
Sync boilerplate↓ ~90%
Optimistic UIAtomic

THREE LAYERS · ONE TOOLKIT

Prepaint · Local-First · Tx

Pick the layers you need. Use Prepaint for revisit speed, and add Local-First and Tx for sync and optimistic UI.

Built for React 19 + CSRVite / SPA / internal toolsAdopt each layer independently
PrepaintRender layer
LAYER 1

Capture the last screen as a DOM snapshot and restore it before React loads.

  • Store full-screen snapshots in IndexedDB
  • Restore on revisit before any JS runs
  • Make CSR revisits feel close to SSR
Local-FirstData layer
LAYER 2

Use IndexedDB as the source of truth and automate server sync.

  • Type-safe models defined with zod
  • TTL and staleness metadata built in
  • Background sync for offline-friendly flows
TxExecution layer
LAYER 3

Wrap optimistic UI updates in transactions you can safely roll back.

  • Run multi-step updates as a single transaction
  • Compensating rollback on failure
  • Keep UI state consistent on network errors

HOW IT FEELS

User-facing behavior in your app

SSR makes first visits fast. FirstTx focuses on everything that happens after: revisits, back navigation, and tabbing around, so users see a ready screen instead of a blank one.

Internal tools with frequent revisits

CRM, admin, and dashboard users jump between list and detail all day. Show the previous state instantly instead of reloading each time.

Refreshing in the middle of a task

Local models keep the latest snapshot, so an accidental refresh keeps filters, scroll, and form state instead of starting over.

When optimistic UI fails

Run optimistic updates as transactions. If the server rejects a change, the screen rolls back cleanly instead of getting stuck half-updated.

Handling offline and flaky networks

A simple sync hook turns your data layer into something that tolerates offline and reconnection, without users losing their place.

Sample runtime events (DevTools)Layers: prepaint · model · tx
prepaint.restorePrepaint
Success

IndexedDB snapshot → DOM restore (4 ms)

model.sync.startLocal-First
In progress

TTL exceeded → background sync starting

tx.commitTx
Success

UI update and server request succeeded

tx.rollbackTx
Error

Network failure → compensate and restore UI state

In the FirstTx DevTools Chrome extension, these events appear as a layered timeline you can filter and inspect.

QUICK START

Wire up FirstTx in three small steps

Install the packages, enable the Vite plugin, and wrap your root. You can start with all three layers or add Local-First and Tx after Prepaint.

React SPA / VitePick layers as you go
Install packagespnpm · npm · yarn
pnpm add @firsttx/prepaint @firsttx/local-first @firsttx/tx

# 선택적으로 필요한 레이어만 설치할 수도 있습니다.
pnpm add @firsttx/prepaint
pnpm add @firsttx/prepaint @firsttx/local-first
pnpm add @firsttx/local-first @firsttx/tx
1

1. Enable the Vite plugin

import { defineConfig } from "vite";
import { firstTx } from "@firsttx/prepaint/plugin/vite";

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

2. Swap your entry point

import { createFirstTxRoot } from "@firsttx/prepaint";
import App from "./App";

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

Local-First model and sync hook

Local-First
import { defineModel, useSyncedModel } from "@firsttx/local-first";
import { z } from "zod";

const CartModel = defineModel("cart", {
  schema: z.object({
    items: z.array(
      z.object({
        id: z.string(),
        name: z.string(),
        qty: z.number(),
      }),
    ),
  }),
});

function CartPage() {
  const { data: cart } = useSyncedModel(
    CartModel,
    () => fetch("/api/cart").then((r) => r.json()),
  );

  if (!cart) return <Skeleton />;

  return <div>{cart.items.length} items</div>;
}

Wrap optimistic updates in a transaction

Update the UI first and sync to the server, with a clear rollback path when requests fail.

import { startTransaction } from "@firsttx/tx";

async function addToCart(item: CartItem) {
  const tx = startTransaction();

  await tx.run(
    () =>
      CartModel.patch((draft) => {
        draft.items.push(item);
      }),
    {
      compensate: () =>
        CartModel.patch((draft) => {
          draft.items.pop();
        }),
    },
  );

  await tx.run(() =>
    fetch("/api/cart", {
      method: "POST",
      body: JSON.stringify(item),
    }),
  );

  await tx.commit();
}