Making Pickathon fast
Last time, we played detective. The Pickathon schedule app had stopped loading days before the festival, and the chase turned up a beautiful chain of culprits — a database read that scaled with history instead of state, a question asked 551,551 times, a background helper that had gone quietly blind.
But that case ended on a twist. When we deployed a completely empty app as a control, it loaded in 15.2 seconds against the festival app's 16.3. Every bug we'd fixed belonged to the app; almost all of the wait belonged to us — the platform floor that every app pays, including one with nothing in it.
You don't debug a floor. You pay it down, line by line, like a ledger. This is that ledger.
First, learn to see
We have one metric we treat as the product itself: time to first app glance — the moment something app-shaped is on your screen. Here's the uncomfortable part: no off-the-shelf performance tool can see it. A vibe paints inside a cross-origin iframe, and the web's standard paint metric (LCP) excludes iframe content by spec. Lighthouse, field data, RUM — all of them dutifully reported the paint time of the chrome around the app, and told us everything was fine.
The fix wasn't a smarter vendor. The host page already receives the ground truth — the sandbox posts it app-painted, app-ready, and boot-error messages as the app comes up. A small bridge converts the first of each into standard User Timing marks in the top frame, where every tool can see them: DevTools charts them for free, and one analytics event per boot gives us the field view. When your key moment happens across a boundary, don't shop for a better observer — export the moment to where observers already look.
Now we could measure the floor. When we started, an empty vibe downloaded and parsed about 1.53 MB of JavaScript before it rendered anything. That figure is the dated baseline every line below is measured against — today's cold boot is around 975 KB and falling, with both compilers gone from the critical path. Time to read the shopping list as we found it.
The compiler shipped to people who only read
The single largest item wasn't application code. It was sucrase — a full JavaScript/JSX transpiler — statically bundled into the runtime, 316 KB of it. And it was never needed on a published app: the code a viewer boots is already transpiled at deploy time (you can see it in the served bytes — no raw JSX anywhere). Sucrase's only real caller is the live-editing hot-swap path. Every person who opened a published vibe was downloading a compiler they would never invoke. Its sibling was a Tailwind compiler running in the browser, 283 KB, recompiling styles the server had already compiled — it now stays out of the boot path entirely, deferring until after first paint whenever the server's precompiled CSS is present.
Get posts like this in your inbox
One email field. Real updates. No algorithm required.
The fix nearly went in the obvious, wrong way — and this is the part worth remembering. await import("sucrase") shrinks the bundle beautifully. It also would have broken the first edit of every session: our package pipeline serves only the first build output, so the split-out chunk would have 404'd. Boot would have looked perfect; the failure was parked exactly where no boot test looks. A throwaway spike caught it before the real change shipped, and the working design serves the transpiler as its own properly-addressed package instead. A bundle-size win that a test can't distinguish from a broken lazy-load is the dangerous kind: smaller and still works are two separate claims, and the second one is only checkable against the emitted bundle.
Two import lines, two stowaway libraries
The next two ledger lines were almost invisible in source code. Six symbols imported from our base library's root — a logger, an image resizer, four components — pulled in an entire CSS-in-JS runtime (49 KB) that no published app uses. Two schema types imported from our API-types root pulled in ~35 validation modules (94 KB), of which the caller needed two.
The mechanism is the same in both: a barrel — a module that re-exports everything — where the re-exported modules do real work at load time (defining styles, compiling validators). A bundler will happily drop an unused function, but it cannot drop a module whose top-level code has side effects. Barrels are where tree-shaking quietly gives up. The fix is embarrassingly small — rewrite the import lines to name the defining modules — and the verification is the interesting part: we lexed the emitted, minified bundles for the libraries' string fingerprints, because package names don't survive minification and an earlier source-level estimate had already fooled us once.
The fix we built, measured, and threw away
Here's the ledger line that made us better engineers. 57% of every vibe document we serve is one stylesheet that is byte-identical for every app on the platform — 60 KB of utility-CSS remapping, re-inlined into every document, re-parsed on every load, cached never. It looks like the most obvious performance win you will ever see.
We built the fix: extract it to a shared, immutable, cacheable stylesheet file. The document shrank by 58.9 KB. And every timing got worse — even warm, with the file served straight from the browser's cache, first paint was 12 ms slower. Total CSS parsed: identical either way. The bytes had relocated; the work hadn't — and a separate stylesheet has to be discovered, fetched, and applied as its own render-blocking resource, while inline CSS is simply there during parse.
So we threw it away. Reducing bytes is not reducing time. The variant that survives is the one that eliminates work instead of moving it: emit only the style rules each app actually uses, which cuts the CSS parsed from 107 KB to 48 KB. That's the direction we're shipping.
The cache our own ads could never hit
Two more lines, both about round-trips. A deployed app's own JavaScript was being served unbundled — about twenty separate module fetches, waterfall-style, on a cold load; bundling it at deploy time collapsed them into one. And then the strangest find of the whole effort: a casual question — "does a query param slow a vibe URL down?" — turned out to have the least casual answer available.
Our HTML cache bypassed on any unrecognized query parameter. fbclid, the parameter Meta appends to every ad click, is unique per click. Put together: every paid visitor was a guaranteed cache miss, by construction. Not "this page is sometimes slow" — "no two ad clicks can ever share a cache entry." The plain URL warms to a quarter-second; with a click ID it sat near a full second, forever. The people we were paying to bring in were the only cohort systematically routed onto the slowest path.
The fix is a deny-list of tracking parameters (fbclid, gclid, utm_*…) applied in two coupled places — ignored by the cache key, and stripped from the app's URL — because sharing a cache entry across variants is only safe if stripping them makes the response genuinely identical. A test renders the page with and without the params and asserts byte-identical output, with a second arm proving a real param still changes it, so the assumption breaks loudly instead of silently. And it's a deny-list rather than an allow-list on purpose: unknown params like ?friend=jchris are app data — the anonymous-share mechanism — and must keep flowing.
One more, back inside Pickathon itself: the anonymous render used to fetch all 2,552 documents to show the 330 public ones, filtering in memory. The tempting fix — push the visibility test into the database query — is the one we refused, because the database's string-matching and the access layer's stricter decoder don't agree on malformed data, and a disagreement there isn't a bug, it's a leak. Instead we reordered the pipeline: decide visibility first from the stored access outputs (which carry every permission fact but no document data), then fetch only the survivors. Same predicate, same code, one step earlier — and a fully-private database now costs zero document reads.
The ledger's lesson
Add up the lines: a compiler for people who only read, a second compiler recompiling finished work, two stowaway libraries behind two import lines, a stylesheet optimization that moved bytes instead of removing work, twenty round-trips where one would do, and a cache designed — by accident, but by construction — to miss for exactly the visitors we paid for.
None of these were hard fixes. All of them were subtraction: the floor wasn't slow because something was broken, it was slow because it was quietly doing work nobody needed. That's the general shape of making things fast, and it's why the discipline matters more than the tricks — measure where the moment actually happens, build the empty version, check the emitted artifact, and be willing to throw away a fix the stopwatch votes against.
The floor isn't zero yet; the ledger stays open. But the next person's cold load carries less of our machinery and more of their app — which is the whole point: the fastest app is the one that shows up already done.
Your app, minus our overhead
Describe an app in plain language and it's live in a minute — and getting faster to open every week.
Start building →