Butterfly Effect is an interactive survey and visualization application that transforms responses into personalized scenes and a collective 3D visualization.
Its custom 2.5D-capable Canvas2D renderer uses shared draw functions to generate both the city canvases and reusable offscreen sprites consumed by the Three.js/WebGL layer on the visualization page.
Users simulate changes in the scene through multi-select inputs before submitting their results. Once completed, individual results, among everyone else, appear in the new section with solo/team switch. Users can write a personalized message that would live with their shape or explore the cohort-filtering, logs table, bar graph and per question comparisons.
Jest suites cover state and data utilities, layout rules, ranking logic, caching, and rendering-related utilities across the application.
Under a 2-vCPU/1-GiB Docker limit, k6 verified 10,160 concurrent SSE connections and 225 complete 488-row initial loads per second. Methodology and recorded results
Retained JavaScript objects and observed GPU memory were compared with quantization and material caching enabled and disabled. Recorded results
A scripted Playwright and React Profiler benchmark measured 21% fewer subtree renders after the Zustand migration (709 → 560) and 16.7% fewer component executions after memoization (234 → 195). Benchmark source
Clustering: Uses node:cluster to run the server in separate worker processes that the operating system can schedule across available CPU cores.
Authorization efficiency: Signs a JWT with jsonwebtoken when a survey response is created, allowing server-side verification and avoiding a database round trip.
Production asset resolution: Loads the compiled SSR entry and reads Vite's generated .vite/manifest.json, which is cached in production to resolve hashed JavaScript and imported CSS assets.
Delivery: Serves compiled assets and streams the React SSR document with renderToPipeableStream from react-dom/server for non-API application routes.
API routes: Separates read, write, and SSE route and server health functions.
Request boundary: Uses express.json for size-limited request parsing, then applies origin checks and payload validation before upstream reads or writes.
-
A custom in-memory limiter uses
node:cryptoto create salted SHA-256 keys for client addresses and, where relevant, client, request, or response identifiers. -
Because the server supports multi-process clustering, workers send fixed-window rate-limit checks to the primary process. This keeps counters consistent across workers and prevents clients from evading limits by reaching a different worker.
-
Sends all survey-results with 250 rows chunks, newest to oldest, then keeps each connection open for live patch events if any and heartbeats (to keep connection open).
-
Deduplicates concurrent chunk (snapshot) fetches through a single in-flight request and maintains a shared normalized row cache.
-
Serialization cache: Caches ready-to-send snapshot chunks by section and row limit, avoiding repeated filtering and JSON serialization until the data changes.
Scene Canvas: this folder contains the source code for 2.5D renderer which is the upstream graphics system.
Graph Runtime: this folder contains the Sprite pipeline and 3D visualization that sits between Scene Canvas and Three.js/WebGL system.
Scene Canvas architecture · Graph Runtime architecture
Zustand stores manage canvas runtime, survey data, and UI state through per-field selectors.
Identity and user preferences remain in React Context. Twelve components beneath rendering-heavy parents use React.memo to prevent unrelated state changes from propagating through the component tree.
Zustand and React.memo were benchmarked separately to see what each one actually moved. Results
Connections: A pg.Pool reuses database connections for REST reads and writes instead of opening a new connection for every request. A separate pg.Client stays open for the PostgreSQL LISTEN subscription without occupying a connection from the query pool.
Schema and queries: SQL migration files define the survey_responses table, weight and message constraints, a unique idempotency hash, and indexes for newest-first history reads. A repository module keeps parameterized INSERT, UPDATE, and paginated SELECT queries behind the existing REST and SSE interfaces, then maps database records into the application's survey-response format.
Live changes: An AFTER INSERT OR UPDATE OR DELETE trigger publishes committed row changes through pg_notify. The dedicated listener converts those notifications into upsert or delete events consumed by the SSE feed, keeping connected clients synchronized without polling PostgreSQL.
CMS reads: A server-only @sanity/client queries the gamificationGeneralCopy and gamificationPersonalizedCopy schemas through one GROQ request. Drafts and disabled entries are excluded, results are grouped by copy type, and the Express endpoint caches them for 60 seconds before returning them to the client.
Although this version of the scene engine performed well on desktop and iOS devices, testing on lower-end Android hardware showed that some visual effects and redraw patterns were too expensive across the full device range.
Recent profiling also led to replacing live Canvas2D brightness filters with a cheaper depth-mask overlay path and tuning distance-based bitmap caching.
From that hands-on experience, I started building Canvas Engine: an unopinionated rendering engine. It separates draw instructions from the renderer through a rich .txt-based declarative notation, keeps renderer lifecycle and cache invalidation tightly controlled, and prevents the main loop from overreaching into application logic. It targets WebGPU first, with WebGL fallback support for older devices.
If you have any questions, feel free to reach out to me at: eozalp.efe@gmail.com




