Skip to content

Speed you can feel.

Fast isn’t a setting we flipped on — it’s hundreds of small decisions, from the first byte the server sends to the last second of a deploy. Here’s how we keep RMH Studios quick.

292 KB

Active-language client payload — down from 866 KB

~40 s

Full production build across 7,830 modules

50 ms

Intent-preload — before you even click

40–110 s

Trimmed from every changed deploy

The philosophy

Every millisecond is a decision.

At runtime

Instant, on purpose.

Perceived speed is engineered. These are the techniques that make the app feel like it’s already one step ahead of you.

Seeded, server-rendered first paint

The server resolves your session and the page’s data in a single pass, so the app renders signed-in and populated on the very first paint — no spinner, no flash of signed-out, no client-side waterfall.

SSR · Route loaders

Prefetched on intent

Hover or focus a link and we quietly preload its data 50 ms later; the real click then renders from a warm 30-second cache. Navigation feels instant because the work already happened.

Intent preload

Optimistic everything

Likes, posts, comments, and bookmarks apply the instant you act, then reconcile with the authoritative server record — rolling back only if it truly fails. One shared apply / commit / rollback primitive powers it all.

Optimistic writes

Ship only what’s needed

Routes are code-split, heavy libraries (3D, editors, audio) are externalized from the main bundle, and we load only your language — the eager locale payload dropped from 866 KB to 292 KB.

Code splitting

Images that adapt

Everything is re-encoded to WebP, sized down the wire with srcset, and blurred-up while it loads. Our image proxy is SSRF-guarded and cached immutably for a year.

WebP · srcset · blur-up

Real-time without polling

The feed streams over Server-Sent Events instead of hammering the API on a timer, and a service worker serves immutable assets instantly — keeping the app usable even offline.

SSE · Service worker

The rendering pipeline

Painted before you blink.

The path from a request to a usable screen is where perceived speed is won or lost. Here’s how ours is wired.

Server-side render

The first response is a finished page, not an empty shell waiting on JavaScript. TanStack Start renders on the server and sends HTML the browser can paint straight away.

Session seeding

The root loader resolves your session on the server, so the shell renders signed-in on the very first paint instead of flashing signed-out while a client request catches up.

Route loaders

Each route fetches its data on the server — or on intent-preload — through a shared server helper, so the page arrives already populated. No client-side request waterfall on mount.

Exact-language hydration

Only your active language is serialized into the render payload, so the client picks up in the right language synchronously — no flash of English, no re-render to swap it.

Render discipline

Hot components are memoized and subscribe to only the slice of state they use, so a like or a new post re-renders the one card that changed — not the whole timeline.

The bundle

Download only what this screen needs.

The cheapest byte is the one we never send. We stay aggressive about what actually reaches your browser.

Code-split routes

Every route is its own async chunk, so heavy screens — the 3D games, the code editor — never weigh down the entry bundle for someone who just opened the feed.

Externalized heavy libraries

three.js, Monaco, PixiJS, and Tone.js are split into their own vendor chunks (Monaco is even split by editor area), so you download them only when you reach a screen that actually needs them.

One language, not all

The eager locale payload dropped from 866 KB to 292 KB by shipping only the default language up front; the rest code-split and load on demand.

Dead weight removed

console.log and debug calls are stripped from production, source maps and compressed-size reporting are off in CI, and tree-shaking prunes anything nothing references.

At build & deploy

Fast to build. Fast to ship.

Speed isn’t only what users feel — it’s how quickly we can get a fix in front of them. So we profile and tune the pipeline too.

One parallel build graph

Images build as a single BuildKit DAG (COMPOSE_BAKE), folding the server build under the client build’s shadow — 30–90 seconds off every changed build.

Caches that survive

The pnpm store, the Vite/Vinxi cache, and a shared BuildKit registry cache persist across builds, with disk-aware pruning that keeps the cache warm instead of nuking it.

A lean build context

Half a gigabyte of static PDFs is served off-host, shrinking the build context from ~520 MB to ~30 MB — smaller images, faster pulls, and far fewer cold builds.

A compiler plugin we wrote

Our stub-server-files Vite plugin swaps server-only modules for stubs on the client, so Node-only dependency trees never reach the browser bundle — and we pre-filtered it to cut its own build cost.

Measured, never guessed

Every stage is profiled — client 16 s, SSR 12 s, Nitro 7 s — so we optimize against real numbers and record what worked, and what didn’t.

Inside the build

Forty seconds, measured.

A fast build is a fast feedback loop. Every stage is understood, profiled, and tuned.

Vite + Rolldown

The production build runs three passes — client (~16 s over 7,830 modules), SSR (~12 s), and the Nitro server bundle (~7 s) — for roughly 40 seconds steady-state.

stub-server-files

A Vite plugin we wrote replaces every server-only module with a stub in the browser build, so Node-only trees never enter the client graph. We pre-filtered its resolver to skip ~99% of imports, cutting its own share of build time from 37% to 30%.

Traced server deps

Heavy native and server-only packages are externalized from the server bundle and traced into the output, so the bundler transforms far fewer modules per build.

Parallel service bundle

The standalone realtime services are bundled separately with esbuild, off the critical Vite path, so they never serialize behind the main build.

Getting it live

From commit to production, quickly.

Shipping a fix fast is part of performance too. The deploy is a cache-aware, mostly-parallel pipeline.

  1. 1

    Build in parallel

    Images build as a single BuildKit DAG (COMPOSE_BAKE), so the server image builds under the client build’s shadow instead of after it — 30 to 90 seconds saved on a changed build.

  2. 2

    Reuse the cache

    The pnpm store and the Vite cache persist across builds as cache mounts, and a shared BuildKit registry cache lets separate machines warm each other.

  3. 3

    Keep the cache warm

    When disk gets tight we trim stale cache mounts surgically instead of wiping everything — avoiding a minutes-long cold rebuild of three.js and native modules.

  4. 4

    Ship a lean image

    Half a gigabyte of static PDFs is served off-host, so the build context and runtime image are ~500 MB lighter — faster pulls, faster boots, fewer cold builds.

  5. 5

    Skip needless boots

    Per-deploy container steps that don’t apply to a given change are gated out, trimming another handful of seconds off every deploy.

Measured over time

We optimize against numbers.

Not vibes — profiles. A few of the wins we’ve booked, with the before and after.

Performance improvements with their before and after measurements.
ImprovementBeforeAfterResult
Client language payload866 KB (all languages)292 KB (yours only)−66% eager JS; other locales load on demand
Docker build context~520 MB~30 MB−94%; smaller images, fewer cold builds
Deploy wall-clockserial build + bootsparallel DAG + gated boots40–110 s faster per changed deploy
Opening a conversationfetched the entire inboxloader seeds one participanta whole request eliminated
Build-plugin overhead37% of build time30% of build timea one-line resolver pre-filter

How we know

Measured, not guessed.

You can’t improve what you don’t watch — so we watch, in production and in the pipeline.

  • Real-user Core Web Vitals (LCP, CLS, INP) are collected from actual sessions, so “feels slow” becomes a number we can trend.
  • Runtime errors and unhandled rejections are captured globally, so a regression surfaces in our logs instead of failing silently for users.
  • Every build stage is profiled with wall-clock timing, so we spend effort on the real bottleneck rather than a guess.
  • When an idea doesn’t pan out — and some “obvious” wins don’t — we write down why, so we never chase the same dead end twice.

Under the hood

The clever bits.

A few techniques we built ourselves because nothing off-the-shelf did quite what we needed.

A server/client compiler boundary

The stub-server-files plugin rewrites every `*.server` import to a matching stub in the browser build, so heavy Node-only trees (Postgres, image codecs, auth) are physically absent from the client — enforced at compile time, not by convention.

SSR-exact language hydration

Only your active non-English language is serialized into the server render payload, so the client hydrates synchronously in the right language with no flash and no mismatch — while English needs nothing shipped at all.

An optimistic-action primitive

A single apply / commit / rollback / reconcile hook drives every instant interaction, and de-duplicates its own optimistic item against the real-time event that later confirms it — so nothing ever double-renders.

Disk-aware build caching

Instead of wiping the cache when disk gets tight, the deploy trims stale cache mounts surgically and keeps the expensive layers (three.js, native modules) warm — avoiding minutes-long cold builds.

Questions, answered

The things people ask.

Why does the app feel instant even on a slow connection?

Because most of the work already happened before you asked. The page is rendered on the server, its data is prefetched the moment you show intent to click, your actions apply optimistically, and a service worker serves cached assets instantly. The network is rarely on your critical path.

Do all the animations slow things down?

No. Motion is CSS-driven, runs on the compositor, and never blocks interaction — and it’s switched off entirely for anyone who prefers reduced motion. The scroll-reveals on this very page are progressive enhancement: the content is fully there without them.

How big is the initial download?

Only what the first screen needs: your language and the current route. Heavy libraries — 3D engines, the editor, audio — are code-split and arrive lazily when you reach a screen that uses them, not up front.

Is “optimization” ever done?

No, and that’s the point. We keep the numbers honest, keep profiling, and keep trimming. The fastest version of RMH Studios is always the next one.

Never finished

The fastest version is the next one.

Performance isn’t a milestone we passed — it’s a habit. We keep profiling, keep trimming, and keep the numbers honest. The bar only moves one way.

How we keep you secure