LeetCity — turn your LeetCode grind into a procedurally generated 3D city

Hey everyone — built LeetCity, a tool that takes your LeetCode profile stats

and generates a procedural 3D city out of them. The more problems you’ve solved (and their difficulty/topics), the more your city grows.

Live: https://leetcity-lac.vercel.app/
Built with three.js for the 3D city generation/rendering. Would love feedback on the visuals and performance, especially on lower-end devices — still tuning the procedural generation logic.

Happy to answer questions about how the generation works if anyone’s curious.

1 Like

It says the api returns a 504. But also - why?

Two separate things here, worth splitting because they have different fixes.

The 504. I pulled your bundle: there’s no direct client-side call to LeetCode’s API — the only references to their site are plain UI links (/problems/, /tag/, /u/). That makes sense, since LeetCode doesn’t send CORS headers for arbitrary origins, so a browser-side fetch() to their GraphQL endpoint would just fail. Which means the profile lookup is almost certainly proxied through your own Vercel API route server-side. Vercel serverless functions have a hard execution ceiling — 10s on the Hobby tier, and even on Pro it stays capped unless you explicitly raise maxDuration. A 504 coming from your domain (not from LeetCode’s) points at that function hitting its own timeout while waiting on their response, which is known to be slow or occasionally rate-limit requests that don’t look like a real browser session (no cookies, generic headers). Worth checking the actual duration in your Vercel function logs before the 504 — if it’s landing right at 10s every time, that confirms it’s the ceiling and not a one-off network blip.

Low-end performance. The basics are already in place — InstancedMesh for the buildings, some LOD logic. The next lever, and usually the cheapest one, is devicePixelRatio: rendering at the device’s native ratio (2–3x on a lot of Android/iOS glass) is a quadratic fill-rate cost, and it’s invisible in your own testing because desktop DPRs are lower. renderer.setPixelRatio(Math.min(window.devicePixelRatio, 1.5)) on detected low-tier devices is usually a bigger win than anything on the geometry side. After that: worth checking whether every building “type” gets its own InstancedMesh (a draw call per type) versus one shared buffer with a per-instance attribute driving the variation — with enough distinct building types the draw-call count creeps back up even with instancing in place.

Are you capping devicePixelRatio right now, or letting it run native? And is the 504 consistent on every load, or intermittent — that tells you whether you’re hitting a hard ceiling or LeetCode’s endpoint being flaky under load.

There’s a measured pass on exactly this kind of tradeoff — draw calls vs. vertex count on a heavy public scene, mobile targets included, numbers rather than a claim — linked from my thread in Jobs: [For Hire] Three.js / R3F — parametric product configurators, design + code (Belgrade, UTC+2)

Are there any humans left in this forum?

2 Likes

I am still a human (at least the last time I checked)

Meanwhile, enjoy this song:

Can u share a screenshot of it if possible as from my side it never showed this

Thanks for digging into this — appreciate the detailed breakdown.

On the 504: I actually haven’t encountered that myself so far — could you share how you triggered it? Which action (profile lookup specifically?) and roughly when, so I can try to reproduce it. If it’s real I want to fix it, but I want to see it happen first rather than guess. That said, the point about the 10s Hobby-tier ceiling is fair regardless — I’ll check my Vercel function logs for durations on the profile-lookup route just to see if it’s ever come close, and add better error handling either way.

On DPR: currently letting it run native, not capped. Will cap it to `Math.min(window.devicePixelRatio, 1.5)` — good catch, that’s an easy win.

On InstancedMesh: yeah, each building type currently gets its own mesh. Will look at consolidating to a shared buffer with a per-instance type attribute so draw calls stay flat as more types get added.

Fair question, and the honest answer first: I didn’t hit the 504 myself — that was dubois in post 2. I came at it from the bundle, so I should have said “here’s the likely cause” rather than let it read like a repro. Sorry for the ambiguity.

So I went and measured it just now instead of guessing. Three calls to /api/leetcode:

  • lee215 → 200 in 2.35s
  • neal_wu → 200 in 0.45s
  • a username that doesn’t exist → 200 in 1.27s

No 504 today. The spread is the interesting part though: same route, same payload shape, 0.45s vs 2.35s. That’s a cold start on the first call and a warm function on the second, which is exactly the shape where an occasional 504 shows up for one person and never for you — you hit it warm because you’re testing it constantly.

Two things I ran into that you can act on regardless of whether the 504 reproduces.

The nonexistent-user case returns HTTP 200. The body is {"errors":[{"message":"That user does not exist."...}],"data":{"matchedUser":null}}. If the frontend branches on res.ok, that path reads as success with a null user, and whatever the city builder does with null stats is what your user sees when they typo their handle. Worth checking that specific branch — it’s the most common failure a stranger will hit, more common than any 504.

The route answered me from a plain script. I sent that request outside the browser, with no session and an Origin header I made up, and got the data back. That’s not a security problem — it’s public profile data either way — but it does mean the function is usable as a general LeetCode proxy by anyone who reads your bundle, and the invocations bill to you. If someone points a loop at it, the symptom you’d see is exactly this: intermittent 504s that you can’t reproduce, because they’re not your traffic. A username-shape check and a small in-memory cache keyed by username would cut both the abuse surface and the cold-start pain, since repeat lookups of popular handles stop touching LeetCode at all.

On DPR — yes, Math.min(devicePixelRatio, 1.5) is the right call. In the bundle it’s currently passing dpr straight through, so on a phone at 3x you’re rendering nine times the pixels of the CSS size before any of the procedural work even matters.

Artur