Animating Off One’s Nose to Spite One’s Productivity

Ian McCullough
5 min readNov 13, 2022

I’ve been working on a React Native-based app for a good little while now. It’s not the exact modality of mobile app development I would’ve chosen, but my company was already a couple years into it when I joined. Being cross-platform is important to us, but we have a very, very small engineering staff, so doing native development for two platforms really isn’t an option for us at this moment.

(Image from Google Image Search)

A few months back, I pulled our mobile app’s repo and, all of a sudden, my source-level debugger no longer worked, and the app seemed to be broken in myriad other, genuinely puzzling, ways while trying to debug. Over time, I would come to learn that this was because of a package called “react-native-reanimated”, version 2. The authors of this package have, in the name of reducing latency, adopted React Native’s nascent TurboModules (don’t worry, you don’t need to know what TurboModules are to understand my gripe here — just know that they are new, esoteric, ostensibly marginally faster, but not without their challenges) and also run a completely separate JavaScript engine, on a separate OS thread, in order to achieve their desired ends. Unfortunately, the current debugging support in React Native (via Chrome) does not support source-level debugging in any app that uses this library.

For my part, my first experience of this was one of a jaw-dropping 40:1 reduction in my productivity: We had a bug that I spent, quite literally, 20 hours searching for, using console.log statements, at my team’s recommendation. After becoming thoroughly fed up, and stubbing out all components that used Reanimated v2, I found and fixed the bug in just 30 minutes, using the source-level debugger. The bug had absolutely nothing to do with animation, but our use of Reanimated v2 had disallowed debugging anyway.

I thought to myself:

“These poor souls! console.log-debugging is for barbarians! They’ve never known the joy and productivity of source-level debugging! I shall teach them! Once they see how much more productive they could be, they’ll surely abandon Reanimated v2 and join me in the bliss of greater productivity!”

I took my branch, in which I had stubbed out Reanimated v2, and I held a Zoom call wherein I showed them the magic of breakpoints, variable inspection, run-time expression evaluation, watchpoints, conditional breakpoints… the whole kit and kaboodle. They knew I was fired up about this topic, so I don’t pretend to know how genuine it was, but they all said things like, “Oh, that’s pretty neat!” and “That would be very helpful.” And yet, for some reason, their devotion to Reanimated v2 remained unshaken. I proposed that we downgrade to either the “included” React Native Animated, or to Reanimated v1. I was told by my engineering lead, “You cannot downgrade. Reanimated v2 is the future.” There is apparently no other option. (Never mind that Reanimated already has v3 Release Candidates out.)

In doing my homework on this, I was genuinely surprised to see that the Reanimated v2 developers are completely unrepentant about this. They know very well that their library ruins debuggers for anyone that uses it, and therefore costs probably hundreds of thousands of hours of wasted productivity per year (based on the over 500,000 weekly downloads of it); they just don’t care. They try to disclaim it by saying that “Reanimated 2 is in an early version”, yet here we are, staring down the barrel of v3. When does v2 stop being “an early version”?

If Reanimated v2 were a drop-in replacement for React Native’s Animated library, such that you could switch back to Animated when you needed to debug, or even if Reanimated v2 had a switch where you could say, “I’m willing to endure a dropped frame from time to time to save half a week’s labor…”, I would be fine with it. I concede debuggers aren’t perfect: the curse of debuggers is, of course, the Heisenbug — a bug that shows up when you don’t have debugger attached, but doesn’t manifest when you do — but by all accounts that I’ve seen, the Reanimated folks seem to consider it a worthwhile tradeoff. Why else would they ever have released this? More importantly, how do they debug anything?

I may, some sad day, work on an application where a single dropped frame of animation is so important that it justifies immolating the modern developer experience (and by “modern” I’m referring to the 40+ years that source level debugging has been around.) I’ve certainly worked on apps with requirements in that neighborhood before. I worked on Keynote chart animations for the better part of a decade, where dropped frames were completely unacceptable, but somehow the team that wrote the animation libraries managed to make them work with debuggers. All I can conclude is that these folks value 16ms of dropped frame more than the time and productivity of the developers that want to use their library.

The hardest thing for me, honestly, has been trying to understand my team’s devotion to this poison library. “It’s the future,” they tell me. If that’s true, then frankly, the future sucks. I’d like to think that Google will expand the debugging capabilities of Chrome to be able to accommodate multiple JS engines running on different threads, and to understand TurboModules, and whatever else it takes, but I’m not naive: React Native is a relatively small development community (compared to JS on web, C/C++, Java, Swift, Kotlin, etc.) and Google has other priorities. I’d also like to think that the developers of Reanimated will eventually come to terms with what they’ve done, and give the community a way to work around the sacrifices needed to guarantee perfect, frame-accurate animation, while letting us debug once again. I’m not holding my breath, because they already have a v3 release candidate going, and while there is some apparent efforts in the area of debugging, it’s still got problems (problems that were introduced by Reanimated in the first place). Beyond that, who knows how long it will take for every package that transitively uses v2 to migrate to v3 (as I write this, npmjs.com shows 746 dependents). Maybe it, too, will become “the future” and bring salvation to us all. We can only hope.

In the meantime, I guess all I can do is: console.log(‘Screw Reanimated v2… and the horse it rode in on.’).

As a final note: Before you comment to tell me I should check out Flipper, please know that I already have, and while it is a useful tool, that does lots of neat stuff, it is not the same thing as a source-level debugger (unless you’re using a very different Flipper than I am.)

--

--

Ian McCullough

Software Engineering Manager who also likes doing things outside!