Performance 14 min · 3274 words

What Runs Where: Splitting Fish-Net Simulation and URP Rendering for Browser IO Fat Games

AI Generated

IO fat games in the browser look simple from the outside: lots of players, big maps, constant motion. Under the hood they are a collision of two very different jobs. Fish-Net has to keep world state consistent across the network. URP has to draw a convincing frame on a WebGL client that is already short on CPU, GPU, and memory headroom.

When those jobs blur together, the browser pays for it. Simulation ticks hitch the main thread. Heavy render features spike memory. Client-side “helpfulness” drifts from server truth and you get desync that is hard to debug. The fix is not a single optimise-later pass. It is a hard line drawn early: decide what runs where, then keep every system on the correct side of that line.

This article maps that split for Unity projects shipping fat multiplayer experiences to WebGL. You will see which work belongs in Fish-Net’s simulation and sync path, which work belongs only on the URP client, and how to structure the boundary so networked state stays authoritative while local rendering stays cheap, disposable, and free of gameplay side effects.

Why WebGL Forces a Hard Simulation–Render Split

That boundary exists because WebGL does not give you a second thread to hide behind. A browser IO fat game already stacks high entity counts, continuous VFX, and dense UI on a single main thread that also has to pump the network, advance simulation, and drive the URP pipeline. There is no spare lane. Every system that runs “just because it always did on desktop” is competing for the same slice of frame time the player can feel.

Fish-Net’s tick work—prediction, reconciliation, interest management, and state serialization—has to finish on a reliable cadence or the session desyncs. URP’s camera setup, culling, shadow passes, and post-processing want the same budget to keep the frame looking finished. On a desktop build you often have headroom to blur those jobs together: extra cores, a larger heap, and a GPU that shrugs at overdraw. In a mobile browser the picture flips. Heap ceilings are tight, GPU fill-rate and bandwidth are modest, and the browser itself can stall the tab when you spike allocation or main-thread work. The result is not a soft quality drop; it is hitching, long garbage-collection pauses, and clients that look smooth until the room fills up.

So the rest of this article is not a checklist of toggles. Turning on occlusion culling, stripping a post feature, or raising a tick rate in isolation does not fix a fat WebGL multiplayer title if simulation and presentation still own each other’s work. The real problem is ownership: which systems are allowed to mutate authoritative state, which systems only decorate what the network already decided, and where that line is enforced so rendering stays local, disposable, and free of gameplay side effects. Get the ownership wrong and no amount of URP tuning will save the frame; get it right and Fish-Net can stay authoritative while the client’s render path stays cheap enough for the browser.

What Fish-Net Owns: Authoritative Simulation and Contested State

Rendering can stay disposable and cheap. Fish-Net’s half of that line is authoritative simulation: every value other players can collide with, score against, or lose to must be owned by the network tick, not by whatever the local camera happens to be drawing this frame.

Simulation ownership means contested state and nothing else. Positions and velocities that drive collisions, mass or radius that change hitboxes and scoring, health, combat outcomes, inventories, team assignment, spawn and despawn authority—if two clients could disagree and break fairness, it belongs on Fish-Net. Local-only feel (screen shake, cosmetic trails, UI pulse) does not. The moment a gameplay outcome is decided inside a client-only script “because it was convenient for the animator,” you have already handed authority to the wrong side of the split.

Thin NetworkBehaviours, shell prefabs

Keep the NetworkBehaviour surface deliberately thin. Sync fields, RPCs, and networked events carry state and intent. They should not carry material slots, renderer feature toggles, camera priorities, or post-processing volumes. Those are presentation knobs. Serializing render configuration through Fish-Net blurs the ownership line WebGL cannot afford to blur.

Treat networked prefabs as shells: identity, a simulation root (transform or prediction-friendly body), and the state that defines the entity in the tick. Meshes, skinned rigs, particle systems, and look-only animators live as child presentation objects—or as separately spawned visual prefabs—that subscribe to state changes. The network object does not need to know which URP path is active; the presentation layer does.

Server, host, and pure WebGL clients

In browser-deployed topologies the responsibilities stay asymmetric. A dedicated server or listen-host runs the full simulation and is the sole mutator of authoritative state. Pure WebGL clients never decide contested outcomes for real; they predict where prediction is enabled, apply reconciled state from Fish-Net, and decorate. Even when the host is also a player in the same process, the rule does not relax: host simulation owns truth; the host’s local URP view is still just another client presentation sitting on top of that truth.

What URP Owns: Presentation, Not Truth

URP presentation layer receiving one-way data from Fish-Net simulation state in a browser client

That local URP view is the entire client-side canvas: it never writes contested state, and it should never be asked to. URP’s job is to turn the reconciled snapshot Fish-Net just delivered into pixels the player can see and feel—nothing authoritative, nothing that other clients must agree on.

In practice that means URP owns the full lighting path (forward or the path your Universal Renderer asset is set up for), shadow cascades or 2D shadow casters, every Renderer Feature you stack for outlines, distortion, or custom passes, the post-processing volume stack, and the fundamental choice between the 2D and 3D renderer. Those decisions live in quality tiers, local player prefs, or simple device detection—never in networked fields.

Fidelity stays local unless it is gameplay

A networked “quality level” that forces every peer to the same bloom intensity or shadow distance is a design smell. Visual fidelity is a local budget, not shared truth. The only clean exception is when a visual is gameplay—a fog-of-war mask that actually hides units, a shared arena light that signals round state. Even then, drive the gameplay flag over the network and let each client’s URP interpret it with whatever local quality it can afford. Materials, particle seeds, camera shake, decal placements, and outline thickness stay off the wire unless another player must see the exact same result for fairness.

Keeping presentation local pays off immediately in bandwidth and interest management. Shells stay thin: no need to serialize cosmetic properties that only the owning client cares about. Interest management can ignore pure visual actors entirely, shrinking the set of objects Fish-Net must consider when deciding who observes what. The simulation stays lean; the view stays rich on the machines that can afford it.

That view still has to fit inside a browser. Integrated GPUs, aggressive thermal throttling on laptops and phones, and hard tab memory limits mean your URP asset, renderer features, and post stack must be budgeted for the worst common WebGL target—not the desktop machine you develop on. Strip unused renderer features for WebGL builds, prefer cheaper shadow modes, and treat every full-screen pass as a cost that competes with Fish-Net’s tick work on the same main thread. Presentation is free to look good only after it has proven it will not steal the frame budget the simulation needs to stay authoritative and smooth.

The Gray Zone: Prediction Proxies, Local VFX, and Cameras

That same discipline extends into the gray zone—systems that feel multiplayer because they fire after a networked event, but must stay client-authored so they never touch authoritative state. A hit lands, a player grows, a round ends: Fish-Net delivers the signal (damage applied, mass changed, winner set). Everything that follows on screen—flashes, juice, camera motion, particle bursts—is local presentation unless the visual itself decides contested gameplay.

Prediction proxies and smoothing targets

Prediction meshes and smoothing targets sit on the render side as helpers bound to networked truth, not as second sources of it. A thin NetworkBehaviour still owns the authoritative transform or state snapshot; a local proxy or interpolator reads that truth, eases visual lag, and may run ahead for the owning client’s inputs. The mesh you see can lag, lead, or blend—but it must snap or correct when reconciliation says the server disagreed. Keep those proxies off the network: no syncvars for smoothed positions, no RPCs that push intermediate visual frames. They subscribe to Fish-Net state and decorate it for URP; they do not redefine it.

When feedback should ignore the network entirely

Camera shake, hit flash, growth juice, screen-edge pulses, and similar feedback should almost always ignore the network after the event signal. The server (or host) already decided the outcome; each client plays its own intensity, duration, and falloff based on local settings, device capability, and whether that client was the actor or a spectator. Replicating shake amplitude or flash color wastes bandwidth, couples presentation to tick rate, and forces every peer to feel the same juice even when their frame budget or accessibility prefs differ. Drive these from local listeners on “damage received,” “mass updated,” or “eliminated”—then let URP and your VFX stack own the rest.

What not to replicate by accident

The common failure mode is treating particle systems, animator parameters, or URP volume profiles as if they were contested state. Emitting a burst on every client from a networked “play VFX” field, syncing animator floats for squash-and-stretch, or pushing volume overrides so everyone’s color grade matches looks convenient and quietly destroys the split. Particle seed and lifetime, blend-tree weights for pure cosmetics, and post stack weights belong on the client after an event or a cheap one-shot RPC that says “this happened,” not continuous replication. If two players must see the same prop destroyed, network the destruction authority; let each client spawn its own debris and dust. That keeps NetworkBehaviours thin, interest management sane, and the main thread free for the simulation work WebGL cannot offload.

Network It, Simulate Locally, or Render Only: A Practical Decision Matrix

That same rule scales to every field, event, and visual tick in an IO fat game. Before a system lands in a NetworkBehaviour, a local MonoBehaviour, or a pure URP presentation script, run it through a three-bucket matrix: network it only when the outcome is contested truth; simulate it locally when the client needs responsive motion that still answers to networked state; render it only when nothing about the effect should ever leave the machine.

The three buckets

BucketOwnsTypical IO / fat examplesOn the wire?
Network authoritativeContested state and outcomes every peer must agree onMass, eat/kill events, score, spawn authority, collision resolutionYes — tick-synced or event-synced
Local simulateClient-side motion and prediction helpers bound to networked truthPrediction proxies, smoothing targets, input prediction, soft body followNo — driven by local copies of networked fields
Render-only cosmeticPresentation that never redefines gameplayBlob wobble, juice pulses, camera shake, background parallax, dust trailsNo — never replicated

Walk a few concrete cases. Mass and eat events decide who grows and who dies, so Fish-Net owns them: the server or host mutates the values and replicates the outcome. Blob wobble—the squash, stretch, and idle jiggle that sells a soft body—belongs on the client. It can read the local mass value as a scale hint, but the animation curve, spring constants, and mesh deformation stay off the wire. Background parallax is pure decoration: scroll rates, layer depths, and shader offsets never touch a NetworkBehaviour.

Frequency and observer relevance as off-ramps

Two multipliers push borderline items down the matrix. Frequency asks how often the value changes. A field that updates every physics step is expensive to replicate and usually belongs in local simulate or render-only unless it is true contested state. Observer relevance asks who needs to see it. If only the owning client cares—or if distant peers would cull it under interest management anyway—keep it local. High frequency plus low observer relevance is almost always a reason to stay off the wire, even when the effect “feels” multiplayer.

How the matrix shapes prefabs and assemblies

Apply the matrix at prefab and folder boundaries, not only at individual fields. Shell prefabs hold the thin NetworkBehaviour and the contested state; child objects or separate presentation prefabs own local simulate helpers and URP-driven cosmetics. Folder and assembly layout should mirror the same split—network/simulation code in one assembly, client presentation and URP features in another—so a WebGL client never compiles or loads server-only mutation paths, and so artists can tune wobble, parallax, and post without touching authoritative scripts. When a new system arrives, drop it into the matrix first; the prefab hierarchy and assembly edges then follow without debate.

Anti-Patterns That Couple Simulation to URP Rendering

Anti-pattern monolithic Fish-Net prefab versus thin networked root with local URP presentation

Skip the matrix and the same mistakes show up in almost every fat or IO prototype headed for WebGL: simulation and presentation get wired together on one object, and the main thread pays for it on every tick.

What the coupling usually looks like

The anti-patterns are concrete and easy to spot in a hierarchy. A single NetworkBehaviour owns mass, eat authority, and the MeshRenderer, Animator, particle systems, and material property blocks. SyncVars or RPCs carry wobble phase, flash intensity, trail length, or URP volume weights because “the other players should see the same juice.” Full visual prefabs are spawned as networked objects instead of a thin shell with a local presentation child. Prediction code rolls back animator parameters or particle clocks alongside transform and mass. Each of those choices quietly moves presentation work onto the authoritative path Fish-Net must run every tick.

One leak, three bills

Coupling does not fail in one place—it inflates bandwidth, interest noise, and client heap at the same time. Every visual field on the wire is another dirty bit, another serialize cost, and another reason a distant observer stays interested in an entity they only needed for score or collision. Heavier networked roots keep more components alive in spawn caches and interest sets, so long sessions climb in managed memory even when the visible player count is stable. On the client, reconcile and interpolation paths touch renderers and VFX that never should have been part of the simulation graph, so eat and kill frames hitch while URP and Fish-Net fight for the same main-thread slice.

The refactored shape

The fix matches the ownership split already established. Keep a thin networked root: only contested state, tick logic, and the minimum hooks observers need. Attach a local presentation child (or sibling under a non-networked wrapper) that reads the root’s public state and event callbacks—mass changed, ate target, died—then drives meshes, animators, particles, and camera juice entirely on the client. Cosmetics stay event-driven and disposable; they never write back into SyncVars or prediction buffers. URP renderer features, volumes, and post remain local settings, not replicated truth.

When the anti-patterns are still in place, the symptoms read like a checklist: hitch spikes on eat and kill, memory that creeps upward across a long tab session, and visible desync after reconcile because the client tried to roll back something that was never authoritative. Strip the renderer off the network root and those three tend to fall together.

Build a Workload Map Your Team Can Actually Maintain

Once the renderer is off the network root, the split only stays healthy if you write it down. A workload map is the durable form of everything above: every system listed, an owner assigned, and a budget that matches browser reality. Without it, the next growth mechanic or VFX pack quietly re-couples simulation to presentation.

Step through the map once

Start with a flat inventory of systems—movement authority, eat and kill resolution, mass and score, spawn and despawn, prediction proxies, blob wobble, cameras, hit flash, post stacks, parallax, audio stingers. For each row, mark a single owner: Fish-Net (authoritative contested state), local sim (client-side helpers that never redefine truth), or URP-only (pure presentation). Then attach a budget. Networked rows get a tick cadence and an interest scope. Local-sim and URP rows get a frame-time slice or a quality tier that can drop under thermal or tab pressure. If two owners both claim the same field, the map is wrong—thin the networked root until only contested state remains.

Validate on real WebGL builds

Treat the map as living only if a short validation loop can fail it. Ship WebGL builds into three checks on a regular cadence: long-session heap (leave a tab running through ordinary play and watch for climb that tracks visual churn), dense-area FPS (force the crowded mid-game state where culling, shadows, and interest overlap), and reconcile spikes (instrument frame time around prediction rollback and confirm animator, particles, and volumes are not in the rollback set). When a check fails, the map tells you which owner overspent—not which shader to blame first.

Revisit when the game grows

Growth mechanics, new biomes, and VFX packs are the usual places ownership drifts. Before a feature lands, run it through the same three buckets: does mass or scoring change on the wire, does the client only need a local proxy, or is it camera juice and particles with no networked field at all? Biome lighting and volume profiles stay URP-only unless the biome itself gates gameplay rules. New VFX packs subscribe to Fish-Net events; they do not become NetworkBehaviour state. Update the map in the same PR that adds the feature so the next person inherits ownership, not archaeology.

Minimal template for the design doc

Paste something this thin and keep it filled:

  • System — short name (e.g. eat resolution, blob wobble, biome post)
  • Owner — Fish-Net | local sim | URP-only
  • Budget — tick rate / interest scope, or frame slice / quality tier
  • Signals — events presentation may subscribe to (none if pure render)
  • WebGL checks — heap / dense FPS / reconcile note for this row
  • Last revisited — feature or biome that last touched the row

That is the whole discipline in one artifact. Fish-Net keeps authoritative simulation and state sync; URP stays local, non-authoritative presentation. When every system has an owner and a budget, browser IO and fat games keep a main-thread frame Fish-Net can actually use—and the hitch, heap, and desync checklist from the anti-patterns stops being the default release story.