raxel.studio

Series · Inheriting Armygadget

  1.   01Inheriting a live codebase
  2.   02The debt you don't see
  3. 03Faster than WordPress isn't fast

Journal

Faster than WordPress isn't fast

A few weeks after v1, the client called. "It's faster than the old site," they said. "But not as fast as we expected." That was the actual bug report.

2026-06-30/raxel

A few weeks after the launch, we'd finished the rest of the migration. The Armygadget webshop — which had been living on a separate WordPress site, with the accounting app over here and the storefront over there — was now folded into one place. Payment gateways were wired up. New services were live. The thing was, by any reasonable measure, working.

Then one day, the call: "It's faster than the old site. But not as fast as we expected."

Which is, honestly, one of the better bug reports we've ever received. They weren't comparing to the old WordPress site — that bar was on the floor. They were comparing to the modern, snappy site we'd promised. Faster-than-WordPress isn't fast. That feedback was the actual diagnosis.

First instinct, of course, was to blame the server. Resources, ping, network round trips, every infrastructure dial we could think of. Everything looked fine. And the smoking gun ruled infra out anyway: most of the app was responsive. Only certain pages were slow. The problem was inside the application.

Up to that point we'd been a consumer of the existing API — endpoints were there, they returned data, we displayed it. Now we had to actually look at what those endpoints were doing. We added audit logs, traced a few requests, and the picture came together fast.

The product listing endpoint was doing this:

  1. Fetch products.
  2. For each product, fetch its categories.
  3. For each product, fetch its images.
  4. For each product, fetch its variants.
  5. Run heavy computation in Node over the assembled data — filtering, sorting, deriving fields.

Classic N+1, with a layer of in-memory data crunching on top. Each step waited for the one before it. The endpoint had been written to be readable, not fast, and it had gotten away with that on small catalogs. With Armygadget's full product set, it had stopped getting away with it.

The fix was structurally obvious: push everything into a single database query. Databases are very, very good at filtering, sorting, joining, and aggregating data — fundamentally better than pulling rows over the wire and doing it in your application layer, by an order of magnitude or more. That's what they were designed for.

But there's a real tradeoff. The moment you move logic into SQL, you give up the easy reusability of having it in a TypeScript module. The filter rules and the sort logic now live in the query, not in a shared helper that another endpoint can import. If we ever need the same filter shape elsewhere, we'd have to rewrite the query — or extract it into a stored function and live with that complexity. Both choices have costs.

For this endpoint, the call was straightforward: it's a hot path, speed matters more than reuse, ship it. We wrote a thick layer of tests first — the endpoint was used elsewhere in the app, and breaking it wasn't on the table — then rewrote the query. The new version did the whole job in a single round trip.

The result was the kind of speedup that doesn't need a benchmark to feel — the page just rendered. The next call we got from Armygadget was about something else entirely.

The lesson, restated: vague-sounding client feedback often contains the actual diagnosis. "Not as fast as we expected" wasn't a wishlist. It was a measurement we hadn't taken.

Form fields for the studio:

  • Title: Faster than WordPress isn't fast
  • Slug: faster-than-wordpress-isnt-fasttrip.

The result was the kind of speedup that doesn't need a benchmark to feel — the page just rendered. The next call we got from Armygadget was about something else entirely.

The lesson, restated: vague-sounding client feedback often contains the actual diagnosis. "Not as fast as we expected" wasn't a wishlist. It was a measurement we hadn't taken.