About this talk
In this talk, Milica, a software engineer at Novium, shares her insights on the common pitfalls encountered when building and shipping React Native applications. She focuses on the importance of understanding the impact of small decisions in code, emphasizing that seemingly minor issues can accumulate, leading to major performance problems and decreased user experience. Milica discusses key areas such as component reuse, performance optimization, managing React Native upgrades, and the importance of code ownership within teams. By illustrating her points with real-world examples and lessons learned, she encourages developers to adopt consistent habits and systematic approaches to maintain application quality and team efficiency.
Full transcript
Okay, let's start. Okay. So, I want to start with something honest. You will ship something broken tonight, tomorrow, or at some point at on your career. And this is not a warning, this is prediction. And that's okay, but that's okay if you know what to do next. So, my talk is all about that. Before we start, let me quickly introduce myself. My name is Milica. I am
a software engineer. I work at the company Novium based in the city Nish in Serbia. And also, I am a co-founder of Women in Tech organization in my city Nish, when we support young women who also entered the career in a tech and engineering. And most of my work over the past few years has been around React Native and mobile apps, building and maintaining production apps used
by a million of users. And today, I want to share some of the lessons and production mistakes I learned while building and shipping React Native apps in real teams. So, I want you to look at this slide. On the left side, we have application that is working smoothly. And on the right side, we have the frozen, broken, laggy, flickering. But, the user, when it sees it, it
does not know your code base. They feel it using your application. I want you to scan the QR codes, and if you can vote, which app would you trust the most? The smooth one or the flickering one. Okay, okay. >> [sighs and gasps] >> Okay, the broken up >> [gasps and laughter] >> has some more points now. But, let's just wait. So, the most of us you
vote that the automatically is the smooth up. Why is that? Most of us when we think about the product, we think about the connect product quality and connect that quality with the engineering and software quality. And sometime that is true, but in production, React Native apps, the difference is often not the mistake like architecture big mistake that is the massive one. It's like the smaller decisions repeated
over time like unstability keys, some upgrades that happened and broke something like hidden performance depth, like nothing catastrophic individually, but together they shape the product experience. They shape the experience of your So, React Native itself, it doesn't slow teams down, but decisions decisions you do and decisions you make do because if often the habits you build around React Native is the habits that will slow your application
down. And today, I want to focus on four areas where I've seen to happen repeatedly in production application. First one is component reuse. We all know that components are building blocks in both React and React Native application and component to use is also a big part of your code base. I've seen a lot of projects and a lot of mistakes happen with code base and code base
starts falling apart because of component and it's not starting to fall apart because like big mistakes it start falling apart with quiet one prop at the time. So in one of our projects we had a very common situation. We had a list card, we have a grid card and we had a hero card and one of our engineers said, let's make a reusable product card component. Honestly,
it sounds like a good engineering decisions because we will have less duplication, we will reuse the code and we will have one place to maintain all the shared logic. And at the beginning it actually work really well. The problem was what happened over the time. So one component is trying to become a three products. So we have three situations, we have to support all the three features
and we want to use one component for all the three features. But now I want you to answer me and what will you do? Will you add another prop to support this mode and support all the three situations? Will you split the component? Will you create a wrapper around it or you will leave it for the later? Can you scan the code now and let's see the
voting results. Let's just wait a little bit more. So, you will split the component and that was the right decisions to do. But, what actually we did is we did this and this was the final result we ended up with. We had one component, we had 23 props, we have like more than 60 lines of code and we had a zero documentation. at this point, nobody in
the team fully understand it anymore. And the dangerous part is this didn't happen because of the bad decisions, it happened slowly because it's faster than splitting the So, eventually someone new come join the team and they make what looks like a safe change, but that safe change about this component broke the production because the abstraction was already unsafe. What what it was the right decisions to do
and what most of the you vote was to fix the problem, but not adding more property, but accepting that these were actually different components and different behaviors. Instead of one giant product card component with multiple properties, we split it into smaller focused components. We had the three components, more files, but much more clarity. And this is the law nobody puts in the architecture documentation. Reuse increases coupling,
coupling reduces velocity. Um by this, I don't mean you do never uh reuse, but uh use it consistently. Make that trade-off visible. So, tomorrow for the next sprint, I want you to look at your code base, find when you have the mode prop variance type and ask if this is the component for one component job or for the three component. Split it or leave it, but comment
about that. Okay, the next thing I want to talk about is performance. Performance is often an invisible problem. The one that lives in production and never shows up in the code review. So, quick story. We had a feed and the feed was a list that was rendering a hundred of items. And we tested on the simulators, it was working really fast. We tested on the test devices
we had, it was also working really fast. But the week we shipped the code into the production, the real users with real data hit it and on Android phones it started to slaughter. Same code, but different conditions. This is like really typical and classic problem in React Native when using the flat list and using the list. Everything can look right when testing and everything can look good
in the code review, but when you ship it to the production and when you ship it to the real users, it's not like happening like when you test it. So, I want you to look at this code. How many problems can you spot here? This was the code in our flat list. And there are at least two problems. First is the rendering item. That is the inline
arrow function. Every time the parent re-renders, this with this will create a brand new function. So, flat list sees a new reference, so it re-renders every single row. Second problem here is you can see the key extractor is having the math random. So, every render, every item gets a completely new key, and React thinks the entire list is new, so it will unmount everything, remount everything, and
the whole list will flicker. What's important here is that users don't see your flat list. Users don't know about your key extractor. Users don't know what you wrote on your code, but they feel the application. They see a feed, they see a feed and a list that flickers or is rendering really slow. And they feel your decisions about that. this is the same list, and this is
the problem how we fix it. We extract, of course, the render item. We also fix it removing the math random and adding the string item ID, and also adding the props like window size, max to render per batch, remove clipper subviews, and all is this in documentation, but at the time power point, this code review passed, and it was good until it doesn't. So, my recommendation for
you is for tomorrow is to always test your application on the mid-range Android. Open the React Dev Tools Profiler, scroll your main feed. Like, anything like consistently above 60 milliseconds per frame, start there. These props are starting points. Like, you always need to profile first and then optimize it. Because in that moment when you don't see like 5 millisecond problem that is going to be but that
really fast change and when you use your using a slow Android phones that feel like make a really bad problem for the future. And also React Native gets you like to working app really quickly. You have the fast start. You have one code base but that's like genuine win. Uh starting fast you need to keep your application fast and Android here tends to expose performance depths sooner
than iOS. So simulators hide a lot and small render decisions the kind we just look at compound over months. None of this is the framework fault. It's just the shape of the work. And one of the thing I also learn from the experience is that postponing an upgrade a React Native upgrade was never a technical decisions. It's a never decision that was made by technical team. It
was most often the decisions that were made by product and a product decisions but postponing these upgrades were always cost a lot a team. So the next thing I want to talk about are upgrades and everyone I think it's least favorite team to talk about. So I wanted to ask you to raise your hands if you upgrade React Native irregularly. And I want you now to raise
your hands if you wait until someone forces you to upgrade React Native. That's right. And I also did that. I also waited a long time and my team also then schedule a regular update of React We'll upgrade when we need to. That is decisions that is most commonly made. So, it sounds reasonable. Like it saves upgrade effort until there is real reason. And every team does this
at the same point. But, waiting and waiting for the versions new to come up will make a gap. And that gap will be bigger and bigger with every new version. So, when people say React Native upgrade, what they actually mean is bump React Native. But, they know that you also need to deal with Metro, Hermes, Gradle, CocoaPods, native modules, and third-party libraries. And this is all part
of the one big ecosystem. You cannot just upgrade React Native. You also need to keep in mind all of these in your timeline with scheduling and upgrading React Native application. So, this is the actual example that happened to our team. So, we waited a lot. We waited until the version 0.79. And our React Native version was in 0.68. And that is the big jump that we needed
to upgrade from one version to another. And also the new architecture support was also in the 0.79. So, what we plan is to finish everything by one sprint. What actually happened is that our upgrade lasted for 6 weeks. In the first week, Metro config breaks. It was like breaking changes. And we already at the same start, we lost 2 days immediately. At the week two, we have
like third third-party libraries that was incompatible that needed patches or we needed to remove them or replace them. In the week three, we have the native model conflicts. Android build broke everything. iOS was fine, classic. And also Gradle version hell, we needed to upgrade Gradle, we broke something else. And at the least, we patched everything, run our regression testing, and finally shipped. So, it takes like more
than a double of time to update everything, test everything, and ship to the final user. And the fix is not technical, it's cultural. What happens mostly is the one brave engineer disappears for a week or a sprint and updates everything. But, should actually happen is to have a system, schedule, predicted review. Like, no heroic, just a checklist. So, this is the checklist I want you to have
in mind when scheduling and upgrading your React Native application. So, I will say that always audit all dependencies first, always dedicate upgraded branch, never do your uh upgrade on the main branch. Also, feature flag. Feature flag for every risk surface. Never upgrade doing early release also, and always write down what you broke for next time. The rule is make it boring because boring is safe. So, for
tomorrow, I want you to look your current React Native version, look at the latest version of React Native, count the minor version between you. If there's more than three, then you should definitely schedule an upgrade sprint. this sprint this month like just make it regularly because small frequent upgrades are boring and boring is safe. And the last thing I want to talk about is ownership and this
one is not really about the code. It's about how teams work, but it's probably the one that cost the most over time. this is the statement. Someone will know what this does. Someone will know what this code does in our team. And this is probably familiar to a lot of teams in this room. Someone added a feature flag, ships the experiment, the experiment ends, the flag stays.
A year later, it's still in the code base because the person who added that flag in the code left the team. there is the Slack thread that happens and I think this also happen a lot of time that someone new comes to the team, ask about the feature flag like in this No one in the team is sure what to do about it or what it does
and in the end someone says, "I wouldn't touch it." So, this is the most expensive message in the software. Don't touch it, you will break something. Now I want you to think about your code base. Think of the one file, one specific file that makes your team nervous. You all know which one it is. You don't discuss it. You route around So, what I suggest to do
in this situation is to document the decisions. Make the decision visible. Owner, date, reason, expire, four or five fields that transform a landmine into the documented If you just added the owner, the created date, the reasons, it will really help a lot of the new engineers that come to Because ownership isn't about the blame, it's about who you actually ask. If you can't answer all the questions,
and you should always ask these three questions, what does this do? Why does it exist? And is it safe to change? And if the answer about that code or about that feature flag is I don't know, and I don't know who to ask, that is system failure, not a people failure. And that will happen over the time. So, in this situation, what I want you to recommend
is to document, document Open your feature flags file, one field, remove after in every file in the If this happens, if we document a lot, the Slack conversation that I show you will never happen. So, you don't need a complex system. One date and one name, review all flags monthly. Every flag without an expiry date is the future incident. And for the end, I'm going to describe
a real uh accident accident that happened, and I want you to vote on what will you do. And there are no wrong answers. So, this is the last poll I will show you, and this is the situation that actually happened to our team. So, you ship an upgrade. Everything looks fine at first. Then, a few hours later, Crashlytics starts showing Android crashes on back navigation from one
specific flow. At first, it looks simple. You find the root cause was the deprecated back handler API and it's easy to replace in your own code. But, then you realize that same API is still exist inside multiple third-party libraries the app depends on. So, even after fixing your own code, some production flows are still crashing because third-party libraries are using it. So, what will you what will
your team do in this situation? Do you patch the app locally? Like that is the fastest path, but you risk it because third-party libraries are still using it. Will you roll back the whole upgrade of the React Native and you will lost the one sprint or the 6 weeks of the codes and the work you were doing? Will you patch the third-party libraries? That is safer, but
you don't know how time you will need to patch all the third-party libraries? Or you continue to investigate every back handler usage and your code base. let's see what will you do in this Roll back the upgrade. Yeah. That is the safest choice to do, but you will lose all the work you were doing all the weeks ago. What we did in this situation, we chose the
C. So, we patched all the libraries. It took us all rest of the day to do that. And what we realized in that process is that the crash itself was not the hardest part. The harder part was discovering that the same back handler pattern existed in multiple places implemented by different by different teams and different libraries. So, one used remove event listener inside component will unmount, another
wrapped it in a custom hook, and in another library buried it inside a model dismiss flow. Three libraries, three patches, three different fixes, and after that incident, we changed two things. First, every React Native upgrade started with a dependency audit before implementation. Second, we standardized how back handler and similar platforms APIs were used across the code base because the real problem was not the API removal itself.
The real problem was that the same problem has been solved different across the ecosystem for years, and the upgrade exposed it all at once. So, React Native doesn't make team fast, it make habits visible. Smooth app, broken app, same technology, same team size. The difference was habits. Habits the team do, decisions decisions the developers make, and system. And that's what this talk was all about. one image
I want to leave with you is that your code base is not just a code, it's a record of every decision your team repeated, every shortcut, every unclear component, every to-do without an owner. They state, and eventually the code base teaches the new engineer how to behave. That's the real thing we ship. Thank you very much. So, you have the the QR code, you can scan it,
and if you want to add it