How We Build Lightweight, Fast Web Applications at Shrestha Co
Six rules that keep our products small: no engine unless the frame rate demands it, pure logic separated from rendering, synthesised audio, per-feature code splitting, and treating every dependency as a permanent cost.
Most web apps are slow for boring reasons. Not algorithmic ones — nobody's sorting a million rows on the main thread. They're slow because they downloaded a megabyte of JavaScript to render a page that three CSS rules could have handled.
These are the rules we actually build to. They're stated as constraints rather than principles because that's how they work in practice: they stop you doing things.
1. Assume a slow phone on a metered connection
Most of OmniPlay's traffic is mobile, much of it on mid-range Android hardware in Nepal. That single fact settles a surprising number of arguments before they start.
A 400KB JavaScript bundle isn't a download problem on a modern connection — it's a parse and execute problem on a four-year-old phone, and that cost is paid on every cold start, not just the first. Developing on a fast laptop over fibre hides this completely.
So: test on real hardware, throttle the network, and treat the slow device as the target rather than the edge case. If it's good there, it's good everywhere.
2. No engine unless the frame rate demands it
The reflexive choice for a browser game is canvas plus a game engine. We use neither.
Every board, token, snake and ladder in OmniPlay is inline SVG styled with CSS.
There's no render loop — state changes, the DOM updates, CSS transitions
animate transform and opacity on the compositor thread. The browser is
extremely good at this, and it's code we didn't write.
What that decision returns:
- Sharp at any device pixel ratio, with no scaling code
- Animations that don't touch the main thread
- Pieces that are real elements — inspectable, labellable, reachable by a screen reader
- No engine in the bundle
Canvas is the right call when you're animating thousands of things continuously. A Ludo board has sixteen tokens. The threshold is much further away than people assume.
3. Separate the rules from the rendering
Each game in OmniPlay is a "cartridge": a pure rules engine with no React in it at all, plus a component that renders it.
The engine exports functions like newGame, applyRoll and applyMove. They
take state, return new state, and emit events — capture, ladder, bounce,
finished. They import nothing from the framework.
// engine.js — no React, no DOM, no side effects
export function applyMove(state, playerId, tokenId) {
// ...returns { state, events }
}The component subscribes to those events and turns them into animation, sound and toasts.
This isn't architectural taste, it's what makes the thing testable. Fifty-eight unit tests cover the engines and the computer opponents, and none of them mount a component or touch a DOM. They're milliseconds each, which means they run on every save.
The second benefit shows up later: a bot is just another consumer of the same pure functions. Game rules and game presentation change for completely different reasons and at completely different rates, and keeping them apart means one can't break the other.
4. Generate assets instead of shipping them
OmniPlay has no audio files. Every sound — the die knocking against the board, a capture, a ladder climb — is synthesised at runtime through the Web Audio API.
An oscillator, a gain envelope, a filter. Tens of lines of code where a sound pack would have been a few hundred kilobytes, and it's more flexible: pitch and duration become parameters rather than separate files.
The same logic applies to fonts. Self-host them. A Google Fonts link is a DNS lookup, a TLS handshake and a request to a third party in your critical rendering path — and a dependency that has to be reachable for your "offline" app to render correctly. Serve the font from your own origin and it precaches with everything else.
5. Split by feature, not by vendor
The default bundler split — your code in one chunk, node_modules in a
"vendor" chunk — optimises for the wrong thing. It produces one large file that
changes whenever any dependency changes.
OmniPlay splits per game. Each game's engine, board, physics and sound are lazily imported, so opening the home screen downloads none of them. Choosing Ludo fetches only Ludo. Adding a fourth game adds a fourth chunk rather than growing the bundle for everyone playing the first three.
The rule generalises: the initial download should contain what's needed to render the first screen, and nothing else. Everything past that loads when a user's action proves they want it.
6. Treat every dependency as permanent
The most effective performance work happens before any code is written, at the
moment someone reaches for npm install.
Every dependency is a permanent cost: bundle size, a transitive tree you now own, a security surface, an upgrade treadmill, and a behaviour you no longer control. Sometimes that's clearly worth it — we use Workbox rather than hand-writing a service worker, because the failure modes there are subtle and tedious.
Often it isn't. This site has no animation library, no icon library, no component library and no CSS-in-JS runtime. Icons are inline SVG at the point of use. Article prose is styled with about eighty lines of CSS rather than a typography plugin. Markdown is rendered and syntax-highlighted at build time, so an article adds no client-side JavaScript of its own — no MDX runtime, no highlighter shipped to the browser. The framework runtime is still there, and it's most of the ~105KB an article page loads. Worth being precise about: build-time rendering removes the content pipeline from the bundle, not the framework.
The question worth asking isn't "does this library work?" It's "what does this cost every user, forever, and could I write the twenty lines I actually need?"
What this actually gets you
None of this is clever. There's no unusual technique here — it's a series of small refusals: no engine, no audio files, no vendor chunk, no library for something a function would do.
The result is a game platform that loads in about a second on a mid-range phone, plays a full game with the network off, and installs to a home screen without an app store. Not because anything was heavily optimised, but because there wasn't much there to optimise.
Fast is mostly the absence of slow.