I was three weeks out from a major release at Synapsis Medical Technologies when our real-time ECG visualiser started dropping frames on mid-range Android devices. We were using the standard React Native View and Path components from react-native-svg to render live waveform data from wearables. As the data points scaled, the bridge traffic spiked. The UI thread would lock up for 200ms every time the chart updated, causing the entire app to stutter.
We couldn't stop feature development for a month to rewrite the rendering layer. We had clinical AI features in flight and a HIPAA-aligned RAG pipeline to maintain. A total rewrite of our visualisation engine would have cost us two weeks of dev time and a high risk of regression in the clinical data display.
Instead of a "big bang" migration, we moved to react-native-skia (version 0.1.x at the time) by running it side-by-side with our existing SVG implementation. This allowed us to verify the rendering accuracy against our FHIR-compliant data sources before switching the flag.
Why the bridge fails at high-frequency rendering
In a standard React Native architecture (pre-Fabric or without JSI-based libraries), every time you update a component, the data travels across the bridge. If you are rendering a 60fps waveform, you are serialising and deserialising a massive JSON object of coordinates 60 times a second.
react-native-skia bypasses this by using JSI (JavaScript Interface) to provide direct access to the Skia graphics engine in C++. The reason you migrate isn't just "performance"—it's memory stability. In our case, the react-native-svg approach saw memory usage climb by 150MB over ten minutes due to the sheer volume of stringified path data being held in the bridge queue.
The incremental migration fix
This process allows you to swap the rendering engine for specific high-cost components without touching the rest of your UI tree.
1. Identify the draw-call bottleneck
Do not migrate your entire app. Use the Flashlight tool or the built-in React DevTools Profiler to find the specific component where the "Commit" phase is exceeding 16ms. In our case, it was the WaveformContainer.
2. Create a Dual-Engine Wrapper
Instead of replacing the component, wrap it. This allows you to A/B test the rendering and fall back instantly if the Skia C++ layer crashes on a specific Android NDK version.
// WaveformRenderer.tsx
import { Canvas, Path, Skia } from "@shopify/react-native-skia";
import { Svg, Path as SvgPath } from "react-native-svg";
interface Props {
points: string;
useSkia: boolean;
width: number;
height: number;
}
export const WaveformRenderer = ({ points, useSkia, width, height }: Props) => {
if (useSkia) {
// We pre-parse the path to avoid doing it inside the render cycle
const skiaPath = Skia.Path.MakeFromSVGString(points);
return (
<Canvas style={{ width, height }}>
<Path path={skiaPath} color="blue" style="stroke" strokeWidth={2} />
</Canvas>
);
}
return (
<Svg width={width} height={height}>
<SvgPath d={points} stroke="blue" strokeWidth={2} />
</Svg>
);
};
How to confirm: Run the app on an Android emulator with "Show GPU Overdraw" enabled. The Skia Canvas will appear as a single drawing layer, whereas the SVG approach will often show multiple nested boxes if you are using complex groups.
3. Move to Skia Values for Animation
If you pass React state into a Skia Canvas, you are still hitting the React render cycle. To get the 60fps performance, you must use useSharedValue from react-native-reanimated or useValue from Skia.
import { useValue, Canvas, Circle } from "@shopify/react-native-skia";
const MyComponent = () => {
const cx = useValue(0); // This stays on the UI thread
// Confirm this works by checking that the React component
// does NOT re-render (log a message in the body)
// while the animation runs.
return (
<Canvas style={{ flex: 1 }}>
<Circle cx={cx} cy={100} r={50} color="red" />
</Canvas>
);
};
4. Shadow Rendering for Validation
Before we shipped to clinicians, we ran both engines in a hidden internal build. We rendered the Skia output and the SVG output on top of each other with 50% opacity. If they didn't align perfectly, we knew our coordinate scaling logic was flawed.
The cost of the Skia abstraction
Skia is not a silver bullet. Adding @shopify/react-native-skia adds approximately 4MB to 8MB to your final APK/IPA size because it bundles the Skia binary.
In my experience, you should avoid Skia for:
-
Simple icons: The overhead of the Skia context is higher than just using a standard
ImageorSVG. -
Text-heavy layouts: Skia's text engine requires you to manage typefaces and glyphs manually. If you need accessibility features like screen readers to work out-of-the-box, stick to the standard
Textcomponent. We kept all our clinical labels in standard React NativeTextcomponents and only used Skia for the waveforms.
At your level
Starting out:
Focus on understanding the difference between the "Main thread" and the "JS thread". Use react-native-skia only when you see the JS thread FPS drop in the debug menu. Start by converting a single static shape before trying to animate.
Working engineer:
Implement a rendering strategy pattern. Don't hardcode Skia components. Use the wrapper approach I detailed above so you can toggle the engine via a remote config (like Firebase or LaunchDarkly). If a specific device manufacturer has a broken OpenGL implementation, you can kill the Skia engine for those users without a hotfix.
Senior or staff:
Audit the memory lifecycle of Skia objects. C++ objects created via Skia.Path.Make() are not managed by the JavaScript garbage collector in the same way. You must ensure you aren't recreating these objects inside a useMemo that triggers too frequently, or you will see the native memory footprint of your app steadily climb.
Lead or director:
Evaluate the impact on your CI/CD pipeline. Adding Skia requires native compilation. When I cut our release cycles from 2 days to 4 hours, one hurdle was the increased build time for native modules. Ensure your runners have enough concurrent power to handle the C++ compilation, or use pre-built binaries if your environment allows.
In the interview
The Question: "How do you handle high-frequency data visualisation in React Native without dropping frames?"
Weak Answer: "I would use Skia because it's faster and uses the GPU." This is weak because it doesn't explain why or what the trade-offs are regarding bundle size and complexity.
Strong Answer: A strong answer identifies that the bottleneck is usually the bridge or the React render cycle. You should discuss moving the execution to the UI thread using JSI-based libraries like Skia or Reanimated. Mention the specific trade-off: you gain rendering performance but lose the high-level accessibility and layout features of the standard View system.
The Senior Follow-up: "How do you handle the memory management of C++ backed objects in Skia?"
The interviewer is looking for your awareness of the JSI lifecycle. Someone who has actually done this in production will talk about avoiding object creation in the render loop and the risks of memory leaks when passing large data sets from the JS heap to the C++ heap. They might mention that while JSI helps, you still need to be careful about the size of the data being passed across the JSI boundary if it's happening 60 times a second.
Amit Chakraborty is a founding engineer and senior architect — React Native, AI/RAG systems and production architecture. Portfolio: www.amitchakraborty.dev · LinkedIn · GitHub. Open to senior and founding engineering roles, remote worldwide.
Top comments (1)
The side-by-side period is the part people underestimate. Curious about the per-frame decision of what stays on the React Native view layer vs what goes to Skia — did you draw the line at component boundaries, or was it more granular once you profiled where the bridge traffic actually spiked? We hit the same 200ms frame-lock pattern on a different visualizer and are weighing the same migration.