<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Rémi Couture</title>
    <description>The latest articles on DEV Community by Rémi Couture (@remicouture).</description>
    <link>https://dev.to/remicouture</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4203461%2F756e2f95-6832-41e5-b8b3-ff42e693c13c.png</url>
      <title>DEV Community: Rémi Couture</title>
      <link>https://dev.to/remicouture</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/remicouture"/>
    <language>en</language>
    <item>
      <title>Shipping a 3D family party game in the browser that still runs on a cheap phone</title>
      <dc:creator>Rémi Couture</dc:creator>
      <pubDate>Fri, 23 Oct 2026 21:55:45 +0000</pubDate>
      <link>https://dev.to/remicouture/shipping-a-3d-family-party-game-in-the-browser-that-still-runs-on-a-cheap-phone-4ckk</link>
      <guid>https://dev.to/remicouture/shipping-a-3d-family-party-game-in-the-browser-that-still-runs-on-a-cheap-phone-4ckk</guid>
      <description>&lt;p&gt;Sloth Jam is a free cozy party game that runs in the browser at &lt;a href="https://www.slothjam.com" rel="noopener noreferrer"&gt;slothjam.com&lt;/a&gt;: 64 one-minute party games, a five-track kart race, and a whole island to explore. I make it on my own. The audience is families, which means the device in the room is often an old tablet or a hand-me-down Android phone, not a gaming PC.&lt;/p&gt;

&lt;p&gt;This post covers the stack and the handful of tricks that keep it playable on those devices.&lt;/p&gt;

&lt;h3&gt;
  
  
  The stack
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;TypeScript + Three.js (r186)&lt;/strong&gt; for the client, bundled with &lt;strong&gt;Vite&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No engine and no model files.&lt;/strong&gt; There isn't a single glTF or FBX in the repo. Every sloth, tree, kart and building is built in TypeScript from simple shapes with vertex colors.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A shared rules package&lt;/strong&gt; (&lt;code&gt;@party/rules&lt;/code&gt;) in a monorepo, imported by both the client and the server, so match scoring and game rules are written once.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A small Node server using &lt;code&gt;ws&lt;/code&gt;&lt;/strong&gt; for the online town. Online play is in early testing and is off by default until a grown-up turns it on.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A Supabase Edge Function&lt;/strong&gt; for the grown-up family account, which signs in with an emailed code instead of a password.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A service worker&lt;/strong&gt; so the game keeps working offline once it has loaded.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Code splitting: one chunk per game
&lt;/h3&gt;

&lt;p&gt;With 64 mini-games, loading everything up front would be silly. Each game's view lives in its own module and is loaded on demand. The production build ships around a hundred JS chunks, so the first load doesn't pay for games nobody has opened yet.&lt;/p&gt;

&lt;h3&gt;
  
  
  The "toy" look is a material, not a texture budget
&lt;/h3&gt;

&lt;p&gt;The art style is glossy painted plastic. Instead of textures, every surface uses vertex colors under a clear coat that catches a reflected studio sky, plus one soft sun shadow map. Ground, stone and sand get a matte flag so the town doesn't look wet. That gives a consistent look with almost no texture memory, which matters a lot on phones.&lt;/p&gt;

&lt;h3&gt;
  
  
  Draw calls are the real budget
&lt;/h3&gt;

&lt;p&gt;A few rules from the engine config:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Sloths don't cast real shadows.&lt;/strong&gt; A sloth is seven meshes, so eight sloths in the shadow pass would cost 56 extra draws a frame. Each one stands on a cheap blob shadow instead.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cull what the camera can't see.&lt;/strong&gt; Sloths behind the camera are skipped, instanced props outside the view are thinned out, and indoors the town outside the walls isn't drawn at all.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Merge, but respect index limits.&lt;/strong&gt; Static scenery is baked into big merged meshes. One cave hall spiked badly on a Galaxy A56, and the culprit was a bake that went past 65,535 vertices, the most a 16-bit index can address. Big bakes are now split into chunks under that limit.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Quality tiers that pick themselves
&lt;/h3&gt;

&lt;p&gt;There are four tiers:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tier&lt;/th&gt;
&lt;th&gt;Max pixel ratio&lt;/th&gt;
&lt;th&gt;Shadow map&lt;/th&gt;
&lt;th&gt;Shadow redraw&lt;/th&gt;
&lt;th&gt;Post (lens)&lt;/th&gt;
&lt;th&gt;MSAA&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;high&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;2048&lt;/td&gt;
&lt;td&gt;every frame&lt;/td&gt;
&lt;td&gt;on&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;medium&lt;/td&gt;
&lt;td&gt;1.75&lt;/td&gt;
&lt;td&gt;1536&lt;/td&gt;
&lt;td&gt;every frame&lt;/td&gt;
&lt;td&gt;on&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;low&lt;/td&gt;
&lt;td&gt;1.5&lt;/td&gt;
&lt;td&gt;1024&lt;/td&gt;
&lt;td&gt;every 3rd frame&lt;/td&gt;
&lt;td&gt;off&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;lowest&lt;/td&gt;
&lt;td&gt;1.15&lt;/td&gt;
&lt;td&gt;off&lt;/td&gt;
&lt;td&gt;n/a&lt;/td&gt;
&lt;td&gt;off&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Lower tiers also drop building detail and leave out some background characters.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;First guess:&lt;/strong&gt; on boot, the game reads the GPU name through &lt;code&gt;WEBGL_debug_renderer_info&lt;/code&gt; and checks it, along with &lt;code&gt;navigator.deviceMemory&lt;/code&gt; and &lt;code&gt;hardwareConcurrency&lt;/code&gt;, against a short list of patterns for older Mali, Adreno and PowerVR chips and software renderers. That picks a starting tier.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Then it measures.&lt;/strong&gt; The game loop runs a fixed 60 Hz simulation step and averages real frame times every two seconds. If frames are too slow for the current tier, it first lowers the render scale to 85%, then drops a tier, and finally goes to 70% scale on the lowest tier. On the two lowest tiers, a device that still can't keep up is capped at a steady 30 fps, which feels better than a jittery 40.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Then it remembers.&lt;/strong&gt; The chosen tier is saved in &lt;code&gt;localStorage&lt;/code&gt; with the GPU name for seven days, so the next visit starts at the right level instead of stuttering first. A &lt;code&gt;?quality=&lt;/code&gt; URL parameter overrides it all for testing.&lt;/p&gt;

&lt;h3&gt;
  
  
  A fixed timestep that forgives slow frames
&lt;/h3&gt;

&lt;p&gt;The simulation ticks at 60 Hz, separate from rendering. A long frame is clamped to 0.25 seconds, and the loop runs at most five catch-up ticks, so a hiccup on a weak device slows the game a little instead of teleporting everyone. Rendering interpolates between ticks.&lt;/p&gt;

&lt;h3&gt;
  
  
  Phone-friendly details
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;The pixel ratio is capped (2 on the top tier), because a 3x phone screen at full resolution is a lot of pixels for very little visible gain.&lt;/li&gt;
&lt;li&gt;The page is a PWA: add it to your home screen and it opens full screen like an app.&lt;/li&gt;
&lt;li&gt;Controls are one stick and one button on touch, keyboard and gamepads, through the Gamepad API.&lt;/li&gt;
&lt;li&gt;The service worker never caches the account server, so sign-in always hits the network.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  What I'd tell someone starting a browser 3D game
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Pick your worst target device on day one and test on it every week.&lt;/li&gt;
&lt;li&gt;Count draw calls before you count triangles.&lt;/li&gt;
&lt;li&gt;Let the game measure itself and step down quietly. Players never see a settings menu.&lt;/li&gt;
&lt;li&gt;Split code by feature so the first load stays small.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Sloth Jam is free and in early access at &lt;a href="https://www.slothjam.com" rel="noopener noreferrer"&gt;slothjam.com&lt;/a&gt;. If you try it on an older phone and it stutters, I'd love to know the model.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Disclosure: I design and direct Sloth Jam myself, and I use AI coding assistants as part of building it. This post was drafted with AI help and checked against the code.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>threejs</category>
      <category>gamedev</category>
      <category>webdev</category>
      <category>typescript</category>
    </item>
  </channel>
</rss>
