OverviewProblemsOptimization 1:
Initial Rendering Load OptimizationProblemRoot CauseSolutionOptimization 2:
Preventing Layout Shifts from Animated ComponentsProblemRoot CauseSolutionOptimization 3:
Render Optimization with MemoizationProblemRoot CauseSolution1. Header Memoization2. Category Bar Rendering3. Selected Condition List4. Condition List RenderingOptimization 4:
Design Change for Improved Re-Rendering TimeProblemRoot CauseSolutionOptimization 5:
Design Change for Improved Initial Rendering TimeProblemRoot CauseSolutionImpactPerformance EnhancementsBusiness ImpactConclusion

Overview

Our company's service provides personalized, precision nutrition supplements based on users' health checkup data, current medications, and daily conditions.
Recently, the number of selectable daily
conditions has significantly increased. Additionally, the concept of severity levels (ranging from 1 to 3) was introduced for each condition, resulting in an exponential growth in the total combinations.This caused severe performance issues when entering the "Daily Conditions" page, particularly on low-spec tablets, such as the
Samsung A7 Lite that we choose, where significant lagging occurred.Considering that more condition types are expected to be added in the future, further performance degradation was inevitable.
The company needed to determine whether to address this issue through
hardware upgrades or software optimization. Replacing the tablets would require recalling all deployed units, increasing production costs and reducing profit margins.
Therefore,
software optimization was deemed essential unless absolutely unavoidable.Problems
The UI response time is very slow, causing a laggy and unresponsive experience.
This leads to frustration and touch errors, significantly affecting the overall user experience. Many users have provided feedback highlighting the slowness and dissatisfaction.
Optimization 1: Initial Rendering Load Optimization
Problem


When the page initially loads, the Daily Conditions Screen is rendered at once.
Each category's position is calculated using
onLayout callbacks, which trigger re-rendering of all conditions multiple times (category count * 2). For instance, with five categories, the all
daily conditions components all re-renders five times.The five unnecessary re-renders caused an additional
4760ms of render time.Root Cause

- Each category's position is managed using
React.state, which triggers re-renders when updated.
- Since position syncing between the
category barand theScrollViewrelies on a shared state at a higher-level component (theScreencomponent), the screen itself re-renders along with all its child components unnecessarily.

Solution

Value changes themselves are not a reason to trigger re-renders.
The situations where these values are needed are limited to:
- Category Touch
Events: When a user selects a category directly.
- Scroll
Events: When the currently visible category changes during scrolling.
These scenarios can update the UI directly via event handling, without requiring state changes that would re-render the entire component tree.
Switching from
React.state to React.Ref allows category positions to be stored without triggering re-renders.
This approach eliminates unnecessary renders:
- Previous Load: 952ms (avg) × 5 unnecessary re-render cycles =
4760ms
- Optimized Load:
0ms(no additional rendering)
Key Takeaway
Avoid overusing
React.state. Only use state when value changes necessitate re-renders.
Optimization 2: Preventing Layout Shifts from Animated Components
Problem
Lag occurs during
category bar animation, making the application unusable on low-spec devices.Root Cause

Design requirements caused header space to shrink during scrolling, affecting the entire layout:
- Components re-rendered repeatedly due to layout changes.
- Lack of
debouncingexacerbated the issue, with redundant renders during animation transitions.
- Components lacked proper
memoization, leading to unnecessary recalculations.
Solution

Reconstructed the layout to isolate the category bar animation:
- Fixed Header Size: The
headerremains static, with only text visibility toggling based on the scroll state.
- Fixed Scroll View Size: The
ScrollView's size remains fixed. An empty space was added at the top of its content to allow animation without altering the layout or its dimensions.
- Absolute Positioning: The
category baris positioned independently using absolute positioning, enabling its animation within the added space while preventing ripple effects on other components.
By maintaining fixed sizes and isolating the category bar animation, the layout avoids unnecessary recalculations and re-renders, effectively resolving performance issues.
Optimization 3: Render Optimization with Memoization
Problem

Selecting any condition caused the entire condition list to re-render.
Root Cause
- Lack of
memoizationfor components. Or
- Inefficient
useCallbackanduseMemoimplementations due to captured values changing unnecessarily.
Solution
1. Header Memoization

Since the header content doesn't change, memoizing the
Header component itself is enough to prevent unnecessary re-renders
An interesting point here is that
JSX objects are always re-created during rendering, causing React to treat them as new props every time. Therefore, the JSX computation result itself should be memoized using
useMemo.On the other hand,
Text is a component that may unmount based on conditions.Thus, memoizing this component is not appropriate.
When it unmounts and is removed from the
render tree, the memoized result is lost, so it won't be reused when re-rendered.
The render time, which used to take
22.9ms per re-render, was reduced to 0ms through memoization.2. Category Bar Rendering

When selecting each body condition item, categories that do not change are unnecessarily re-rendered every time, even though they don’t need to be.

The memoization strategy is as shown in the image above.
There was concern that memoizing each button might be excessive, but since each button takes
5.7ms to render and only two buttons need change when the category changes:- Rendering the other three buttons unnecessarily takes about
17msmore.
- Determining the memoization with a simple primitive comparison (
currentIndex === index) involves tiny overhead.
Thus, it was deemed much more valuable to memoize each individual button as well.

Despite using
React.memo, the category bar re-renders every time a body condition is selected.The
categories prop is re-created on every render due to Object.keys(), causing React.memo to treat it as a new value, triggering unnecessary re-renders.
The render time, which used to take
65.8ms per re-render, was reduced to 0ms through memoization.
When a selected button changes, only the previously selected button and the newly selected button's UI should be updated. The other buttons should use the memoized version.

The issue wasn’t just about wrapping the component in
React.memo. The main problem was that the function passed as a prop was re-created on every render, leading React to interpret it as a prop change and triggering unnecessary re-renders.To handle functionality based on each
category's index, a function was created to process the index. Since the default onPress callback for the button component does not provide the index as a parameter, the function was designed to return a handler for the specified index.However, this approach results in a new function being created on each render, even though the logic remains unchanged and covered with useCallback.
As a result, React perceives the function reference as different and re-renders the component.

I resolved this issue by creating a separate button that accepts the
index as prop, and memoize this component.
As a result, when selecting a category, only the changed parts are re-rendered. This optimization improved the render time from the previous
37.8ms to 12.7ms.
The category bar now only re-renders when
categoryIndex changes.Additionally, when the
categoryIndex is updated, only the two buttons that are affected by the change are re-rendered.3. Selected Condition List

The list changed frequently, so the child components were already wrapped with
React.memo for memoization. However, they were not actually being memoized.
The
value changed by the setter function was included in the dependency array, causing the function to be re-declared every time the value changes, leading to unnecessary re-renders.Modified the code to reference the
current value at the time of function invocation, rather than using a captured value from a closure.
Each "condition" cell takes approximately
27ms to render.As a result of memoization, the more condition items a user selects for the day, the more noticeable the performance improvement becomes.
For example, with 10 condition items, the rendering time was significantly reduced from
217ms to 29ms thanks to memoization.
Additionally, I replaced the
FlatList with a regular ScrollView for rendering the list.While
FlatList is excellent for rendering large numbers of components in a scrollable view by leveraging virtualization for optimization, this use case involved only 5 to 10 components at most, representing the user's selected daily condition. Given this, I concluded the overhead from computations required for virtualization outweighed its benefits.
Using the Profiler, the rendering process showed a reduction in unnecessary hierarchical structures, resulting in a cleaner implementation. Besides, there was a slight improvement in rendering time:
38.4ms → 28.8ms 
Only the newly added or changed body condition are re-rendered, while the rest of the components are reused from
memoization, improving performance. Great!4. Condition List Rendering

This is the key part that requires optimization. There are a total of 45 different daily conditions, and each condition can have a
severity change of 1 to 3.These cells are all re-rendered unnecessarily every time the user interacts.

Through memoization, only the list component and the single cell interacted with by the user are re-rendered. This improved the performance from the previous
940ms to 178ms.As a result of memoization, the rendering time improved significantly, reducing from
986ms to 220ms, and the responsiveness to touches has noticeably increased.However, there is still:
- a slight delay perceptible to the user, and
- we must consider the communication and computations involved in real-time interactions with the supplement dispensing engine as well.
Therefore, the current state remains somewhat insufficient.
I’ve decided to continue optimizing with the goal of achieving a response time under 100ms.
Optimization 4: Design Change for Improved Re-Rendering Time
Problem

Two list components refer to a single
Context State, controlling the internal components of each list.The second list component dynamically adds, removes, or changes subcomponents based on the user's selected body condition. Therefore, it makes sense for the list component to control its sub-elements, and memoizing these sub-components is enough to prevent unnecessary re-renders.
However, the first list component always has 45 fixed sub-elements. The only variations are whether they are selected and which severity level (1 to 3) they correspond to.
By understanding this structural characteristic, I believed that most of the responsibilities currently handled by the list component could be delegated to its sub-components, drastically reducing unnecessary computations.

The area marked in the image above represents the part I wanted to improve further.
Root Cause

Currently, the list component controls the individual cell’s props.
The shared state is centralized and referenced by the list component, which is responsible for calculating and passing props such as
isSelected and severity to each cell. As a result, when the state changes, it is inevitable for the list component itself to re-render in the current structure.
Solution

To solve this, values shared among multiple components should remain centralized, but components requiring re-rendering should update their UI independently.
By doing so, the responsibility of calculating props for rendering the sub-components is shifted from the list component to each sub-component. This leaves the list component with only the task of initial rendering.
The solution that immediately came to mind was
Recoil.atomandatomFamilyallow each component to maintain its own state while still being accessible elsewhere.
- A
selectorcan define transformations based on changes to theatom.

Since we were already using
GraphQL with Apollo Client, I was aware that Apollo’s makeVar could serve as a global state management tool. As a result, I decided against adding
Recoil to the stack and instead leveraged Apollo’s internal functionality to implement features similar to Recoil's atom, atomFamily, and selector like the image above.- StateFamily is based on the concept of Recoil's
AtomFamily.
- Each state follows the idea of Recoil's
atom.
isNothingSelectedandselectedListare modeled after Recoil’sselector.

Brilliant! The rendering speed improved dramatically.
To summarize the result of memoization, which impacts every time the user interacts with any ㅇdaily condition cell:
- The rendering time for the entire list improved from
180msto about21ms(an 88.33% improvement).
- The total rendering time improved from about
220msto about42ms(an 80.91% improvement).
As the page itself is no longer affected by unnecessary re-renders outside of the memoized components, additional improvements have been achieved.
The rendering time has improved beyond the target of 100ms, now requiring only
42ms for each body condition selection.The UI delay the user might have felt has been eliminated.
There's nothing more I could wish for.
Optimization 5: Design Change for Improved Initial Rendering Time
Problem

During the initial page rendering, all 45 body conditions were rendered at once, making the initial rendering quite heavy.
As a result, it took around
1800ms for the user to view the first screen, and it needed improvement. The goal was to reduce this time to less than one
1000ms.Root Cause
Rendering all 45 body conditions at once was too much, and since the user wouldn't view all of them immediately, there was no need to render everything from the start. Therefore, rendering optimization through
virtualization was necessary.I was already aware of this, so the list was implemented using the
FlatList component, which offers virtualization optimization. However, it wasn’t effective, and the issue lay in the component design.
Looking at the
renderItem of FlatList, it was rendering by groups, not individual components.
From
FlatList's perspective, it wasn't rendering 45 individual components, but rather just 5 components.Each group contained many number of body condition components, so rendering by groups caused heaviness and didn't take full advantage of virtualization.
Solution

Instead of rendering the 45 body conditions in 5 groups, flatten the component structure to enable row-based rendering, allowing
virtualization to work more efficiently.
With this approach,
virtualization optimization worked properly, and the 45 body conditions were rendered in three stages:- During the initial page render, only the necessary page elements and visible body conditions were rendered for the user to see.
- Shortly after, 20 additional body conditions were rendered behind.
- The remaining 20 body conditions were rendered naturally as the user scrolled.
This resulted in a significant improvement, reducing the time for the first screen to load from
1800ms to just 312ms.Impact
Performance Enhancements
- Faster Initial Rendering
- Reduced initial page rendering time:
1995.6ms→437.8ms(78% reduction) - Achieved through virtualization and optimized list component structure.

- Optimized Interaction Performance
- Reduced re-rendering costs during user interactions:
1100ms→228ms→42ms(96% reduction) - Enhanced efficiency by addressing redundant updates and simplifying component responsibilities.

Business Impact
- Avoided full recall of deployed units, saving extensive logistical costs. Specifically, a recall of 500 engine units would have incurred tablet replacement costs of 10 million KRW and required a 3-month turnaround period.
- Eliminated the need for tablet replacement, reducing production costs. A bulk order of new tablets for upgrades (MOQ) would have cost 100 million KRW, and replacing the tablets would have led to an additional 7.5 million KRW in disposal costs.
- Preserved profit margins by mitigating unnecessary financial burdens, saving 250,000 KRW per unit on the 500 units already produced. This approach ensures a positive impact on margins for future production as well.
Conclusion
This project demonstrates the impact of proper use of
state management and component memoization, component architecture on performance, particularly in low-spec environments.We shouldn't be satisfied with just achieving the desired behavior.
This experience taught me valuable lessons about preventing misuse and overuse by gaining a deeper understanding of the framework or library. It emphasized that using something with clear intent and understanding leads to greater advancement.
tbd
What I did for Rendering Optimizations