One codebase for the web, Telegram, iOS and Android
· 5 min read · Tyamka team
Why Tyamka runs on Expo and React Native rather than Flutter or three native apps, what we measured before choosing, and what the web build costs us.
Tyamka has to run in more places than most small games. It has to open in a browser and inside Telegram, run on iOS and Android, and later on Steam and Steam Deck. On 25 September we settled on one TypeScript codebase on Expo and React Native for all of them. The repository is still called puzzle-game-flutter, because the first brief said Flutter. This entry is about how that changed.
What we compared
We built the same prototype, a 7×7 Schulte table, in several stacks. First Flutter for the web against plain TypeScript in the browser, later Expo with React Native Web. Separately we looked at writing each platform natively in Swift and Kotlin, with its own stack for Steam.
The deciding number was the time until the game can be played on a slow connection. We measured in Chrome with a cold cache and the CPU slowed four times, and took the median of three runs. The slow profile was 562.5 ms of latency at 1.44 Mbit/s, the fast one 150 ms at 8.1 Mbit/s. We set both by hand, so they are close to the DevTools presets but not the same.
| Build | Slow profile | Fast profile |
|---|---|---|
| Plain TypeScript | 2.2 s | 0.6 s |
| Expo, React Native Web | 3.7 s | 1.0 s |
| Flutter web | 13.5–14.4 s | 3.0–3.5 s |
Poki, one of the big web game portals, says players leave when a game takes longer than about ten seconds to load. Flutter was over that line with a single game on the page, and every one of the fifteen games would add code on top. In WebKit even the Flutter Wasm build fell back to its heaviest JavaScript variant, and Flutter's own docs say the Wasm build does not run in any browser on iOS. So Telegram on an iPhone would get the slowest version. For a game people open from a chat, that decided it.
Why not native
Writing each platform natively looked like the quality option. What stopped it was the most expensive part of the project, the game boards and the skins. Most skins change the layout of the HUD, the cells or the results screen, not just the colors, and native would mean building all of that three times. Swift also does nothing for Steam on Windows or Steam Deck. Native did win on paper in one place. JavaScript animations in a web view on the iPhone, which is where the web build runs inside Telegram, stay at about 60 Hz, and there is no public way to raise that.
Expo was not in the first round. A reviewer suggested it, so we measured it on the same Schulte table. At 3.7 s on the slow profile it was slower than plain TypeScript but well under ten seconds, with about 300 KB sent over the network. The same prototype also ran on the iOS simulator and an Android emulator as real native apps.
We did not measure on phones. There were no test devices, so the choice was made on desktop browser numbers with throttling.
What we got
- One TypeScript codebase. iOS and Android build natively from it, and the web build goes to the browser, to Telegram and to game portals.
- Steam and desktop planned as the same web build inside Electron. A small Electron prototype got Steamworks achievements and cloud saves working on macOS. The overlay and Windows have not been checked yet.
- The rules of every game in plain TypeScript modules with no React import. Randomness and time come in as arguments, so the same seed gives the same level on any device. Later this let the server check a result by replaying it with the same code, which has its own entry.
What it costs
Expo releases break things often, and new iOS versions reach it with a delay. Game Center, Google Play Games and good haptics will need small native modules of our own.
The web build is a platform of its own, not a by-product that comes for nothing. The Expo prototype had six bugs. Two existed only on the web, two more showed up more clearly there, and the last two lived on one mobile platform each. Three web quirks are now written into the project rules.
- React Native Web ignores presses shorter than 50 ms unless the press delay is set to zero. In a reaction game that silently eats real taps, so the shared tap component sets it to zero on the web.
- A web export that runs from a portal subfolder needs relative paths. Native builds must not get that setting, or Android stops loading bundled images, so it is switched on only for the web export.
- Forget the safe-area provider at the root and the web page is blank.
Every web change is checked in Chrome and in WebKit, never in one browser only.
Where it stands
All fifteen games play in the web build from one shell. We started an automated playthrough on iOS, and on an iPhone 16 Pro simulator with iOS 18.6 level 1 of eight games passed without a bug in the game code. On 26 September we paused the native work. The first real test is on the web and in Telegram, and iOS and Android come back after it. Until then nothing in the code may be web-only.
The Schulte table we used for the measurements is on the games page with the other fourteen.