If you've ever opened HappyCharts on your phone and noticed the candle pop-in animation stuttering — that wasn't your phone. That was us making your GPU work way harder than it needed to.
Let me explain what was going on.
The homepage and tournament chart animations were creating hundreds of unnecessary GPU operations. We rewrote them to do the same visual work with a fraction of the cost. Also: the skip penalty is gone.
If you've ever opened HappyCharts on your phone and noticed the candle pop-in animation stuttering — that wasn't your phone. That was us making your GPU work way harder than it needed to.
Let me explain what was going on.
When you open a tournament or land on the homepage, those candles do a little pop-in animation. Each one scales up with a bounce, gets a star sparkle, and settles back down. Looks cool. The problem was how we built it.
For every single candle, the code created three separate animation entries on the timeline: one for the star burst, one for the scale up, one for the elastic settle. Multiply that by 102 candles and you've got 306 individual animation entries that the browser's animation engine has to track, schedule, and execute. On top of that, each candle was creating its own temporary star SVG elements — 264 of them in total — all with individual position calculations.
On a decent desktop? You wouldn't notice. On a phone from 2022 or an older laptop? Frame drops. Jank. That split-second where everything freezes and then catches up. Not the vibe we're going for.
GSAP, the animation library we use, has a built-in feature called "stagger" that's specifically designed for this. Instead of creating 306 timeline entries by hand, you tell it: "animate all these elements with this stagger delay between them." Two entries. Same visual result. The animation engine handles the rest internally, way more efficiently than our hand-rolled loop.
The star sparkles still happen on every candle — we tried reducing them but it looked lifeless. The difference is that they're now created inside the stagger callback instead of as separate timeline entries. Same amount of stars, fraction of the overhead.
We also fixed something dumb while we were in there: the code was reading candle positions after setting them to invisible (scale: 0). Which meant getBoundingClientRect() was returning zero-size rectangles. The positions happened to still work because of how CSS transforms affect bounding boxes, but it was wrong and could break in edge cases. Now we read positions first, then hide the candles, then animate.
The hero text animation on the homepage had the exact same problem, but worse. Each letter — not word, letter — got three individual timeline entries. A typical headline has 30-40 characters. That's 90-120 timeline entries just for the stagger phase, plus another 80 for the final explosion where all letters bounce simultaneously.
Same fix. Per-row GSAP stagger instead of per-letter forEach. The finale went from 80 entries (one bounce + one star burst per letter) to exactly 2 entries: one tween for all letters bouncing together, one callback that creates all the stars.
The replay animation during tournaments got optimized too. Smaller impact since it handles fewer candles at a time, but same pattern: batch operations instead of per-element loops.
| What changed | Before | After |
|---|---|---|
| Chart candle timeline entries | 306 | 6 |
| Hero text timeline entries | ~200 | ~8 |
| Replay animation per batch | 3 per candle | 2 total |
You probably won't consciously notice the difference on a fast device. But on slower hardware, the animation should now maintain 60fps instead of dropping to 30-40 during the pop-in sequence. It's one of those changes where "nothing looks different" means we did it right.
This one's simple. When you created a tournament, there was a "Skip Penalty" slider that let you set a percentage penalty for skipping rounds. Default was 1%. In practice, almost nobody changed it, and having a penalty for skipping felt like it was punishing people for making a legitimate trading decision.
It's gone. The slider is removed from tournament setup, private season setup, and the private season creator. The default is now 0% across the board. If you skip a round, nothing happens to your balance. Skip because you don't like the chart, skip because you're not sure, skip because you need to go make coffee — no penalty.
The underlying system still supports skip penalties (the calculation logic is untouched), so if we ever want to bring it back as a competitive mode feature, we can. But for now, it's one less thing to think about when creating a tournament.
We also delayed when Mixpanel's screen recording starts on the homepage. Before, it would start recording immediately — which meant it was capturing the entire intro animation. That's wasted bandwidth and storage for something that looks the same every time. Now it waits 4 seconds for the animation to finish before starting the recording. Same recording coverage for actual user interactions, less noise in the data.
Go play a tournament. The animations look exactly the same. They just don't make your phone sweat anymore.