I used to think performance problems were always something dramatic. A missing index. A memory leak. Something you'd find and immediately understand, the kind of bug that makes a good story because it has a clear villain.
PennyCanny wasn't like that. Nothing was broken. Nothing was throwing errors. It just felt slow, and had felt slow for a while, in that vague way where everyone notices but nobody can point at the exact cause.
Going In Without a Theory
When I started looking into it, I didn't have a theory yet. The stack was plain enough: Node.js and TypeScript on the backend, Handlebars for templating, plain CSS, no frontend framework carrying the UI. AWS underneath all of it. Nothing exotic, nothing that screamed "this is where your problem lives."
I went through the request lifecycle piece by piece, trying to see where time was actually going. Not guessing, just watching. And a pattern started showing up that I hadn't expected.
What I Actually Found
Work that had nothing to do with the server was happening on the server anyway.
Things that depended entirely on what the browser already knew, client-side state, UI-only computation, logic that should have resolved the moment the page hit the user's screen, were instead being resolved before the response even left the server. Every request was doing extra work it didn't need to do, on every single visit, regardless of whether it was needed right away.
It wasn't a bug in the sense of something broken. It was more like a habit that had built up over time. Handlebars renders server-side, so there's a natural pull to just keep adding logic to the server-rendered template, even logic that has nothing to do with fetching data or enforcing business rules. It's the easy path. Nobody decided to do this on purpose. It just accumulated, one small addition at a time, until it was costing real latency on every request.
Fixing It Without Fixing Anything Big
This is the part that still surprises me a little. The fix wasn't a rewrite. Wasn't a new framework, wasn't new infrastructure, wasn't anything that would show up as an impressive line item in a project summary.
I went through the server logic and separated what actually belonged there, data fetching, business rules, anything that needed to be centralized or trusted, from what didn't. The part that didn't belong moved to the client, where it should have lived from the start.
That's it. The server stopped carrying work that wasn't its job. The client picked up what it should have been handling all along.
The Number, and the Thing Under the Number
Load time dropped 20%. That's the number I'd lead with if someone asked what happened. But the thing I actually think about more is what it took to get there, which wasn't cleverness, it was just going slowly enough to notice where the boundary between server and client had quietly blurred.
A few other rough edges cleaned up as a side effect too, once the server stopped masking a structural issue with raw effort. That part wasn't planned. It just happened once the actual cause got fixed instead of worked around.
What I Keep Coming Back To
I used to think the biggest performance wins came from adding something, better caching, more infrastructure, a faster database. This one taught me the opposite. Sometimes the win is just noticing work that never needed to happen in that place at all, and moving it back to where it belonged.
No framework enforces that boundary for you. Nothing warns you when it starts drifting. Someone has to actually go look.
I work on Node.js, TypeScript, and AWS, usually the quiet performance problems, not the dramatic ones. If your app feels slow and nobody can say exactly why, I'd be glad to take a look.
