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 bar
and theScrollView
relies on a shared state at a higher-level component (theScreen
component), 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
debouncing
exacerbated 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
header
remains 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 bar
is 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
memoization
for components. Or
- Inefficient
useCallback
anduseMemo
implementations 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
17ms
more.
- 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
.atom
andatomFamily
allow each component to maintain its own state while still being accessible elsewhere.
- A
selector
can 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
.
isNothingSelected
andselectedList
are 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
180ms
to about21ms
(an 88.33% improvement).
- The total rendering time improved from about
220ms
to 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:
1100
ms
→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