<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Mohamed Eltohamy</title>
    <description>The latest articles on DEV Community by Mohamed Eltohamy (@imcitizen13).</description>
    <link>https://dev.to/imcitizen13</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1053879%2Ffe204c42-f304-4cbe-af92-42361f28e30e.jpg</url>
      <title>DEV Community: Mohamed Eltohamy</title>
      <link>https://dev.to/imcitizen13</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/imcitizen13"/>
    <language>en</language>
    <item>
      <title>My iPhone 16 Pro Stuttered but the S24 Felt Smoother: A React Native Skia Hunt</title>
      <dc:creator>Mohamed Eltohamy</dc:creator>
      <pubDate>Tue, 14 Jul 2026 14:13:41 +0000</pubDate>
      <link>https://dev.to/imcitizen13/my-iphone-16-pro-stuttered-but-the-s24-felt-smoother-a-react-native-skia-hunt-2n51</link>
      <guid>https://dev.to/imcitizen13/my-iphone-16-pro-stuttered-but-the-s24-felt-smoother-a-react-native-skia-hunt-2n51</guid>
      <description>&lt;p&gt;&lt;em&gt;On paper, an iPhone 16 Pro should never stutter rendering &lt;code&gt;192 circles&lt;/code&gt; with React Native Skia. But my screen was stuttering, and the FPS counter said &lt;code&gt;80&lt;/code&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;While testing the animation, I felt a small jitter. I ignored it because it was running in the simulator. Then I ran it on my iPhone 16 Pro, and there it was again: the stutter.&lt;/p&gt;

&lt;p&gt;I ran it on both my iPhone 16 Pro and Samsung S24 side by side, and the S24 showed nothing or at least I didn't catch it, but the iPhone was consistently stuttering in the transition phase of the animation.  &lt;/p&gt;

&lt;p&gt;&lt;iframe class="tweet-embed" id="tweet-2055622804083708159-542" src="https://platform.twitter.com/embed/Tweet.html?id=2055622804083708159"&gt;
&lt;/iframe&gt;

  // Detect dark theme
  var iframe = document.getElementById('tweet-2055622804083708159-542');
  if (document.body.className.includes('dark-theme')) {
    iframe.src = "https://platform.twitter.com/embed/Tweet.html?id=2055622804083708159&amp;amp;theme=dark"
  }



&lt;/p&gt;

&lt;p&gt;I got the inspiration from something I saw on &lt;a href="https://x.com/radiofun8/status/2053588240201101769" rel="noopener noreferrer"&gt;Minsang Choi's X&lt;/a&gt; and Siri border glow.&lt;/p&gt;

&lt;p&gt;Picture &lt;code&gt;192 balls&lt;/code&gt;: &lt;code&gt;128&lt;/code&gt; on the border and &lt;code&gt;64&lt;/code&gt; oscillating, moving like one gooey metaball blob, merging, and separating as the user interacts with the app.&lt;/p&gt;

&lt;p&gt;So I thought of my stack:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Expo, of course&lt;/li&gt;
&lt;li&gt;Skia RuntimeEffect shader&lt;/li&gt;
&lt;li&gt;Reanimated 4 worklets and SharedValues&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;First a quick metric guide as we're gonna repeat this all over the post&lt;/strong&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;What it means&lt;/th&gt;
&lt;th&gt;How to read it here&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;FPS&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Average frames rendered per second&lt;/td&gt;
&lt;td&gt;Good first signal, but it can hide visible stutter&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Frame budget&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Time available to finish one frame&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;120Hz = 8.33ms&lt;/code&gt;, &lt;code&gt;60Hz = 16.7ms&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Missed frame %&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Share of frames that missed the target budget&lt;/td&gt;
&lt;td&gt;Better than average FPS for spotting stutter&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;p50 / p90 / p99&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Median / slower-end / worst-end timings&lt;/td&gt;
&lt;td&gt;Shows whether the problem affects most frames or only the tail&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Encoder time&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;CPU time spent submitting work to Metal&lt;/td&gt;
&lt;td&gt;Useful, but it does &lt;strong&gt;not&lt;/strong&gt; prove shader execution was cheap&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;JS thread vs UI thread&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;React Native JS work vs animation/worklet-side work&lt;/td&gt;
&lt;td&gt;JS can be idle while UI-thread worklets still cause frame drops&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Main rule: &lt;strong&gt;average FPS tells you speed; frame timing tells you rhythm.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The GPU Is the Obvious Suspect
&lt;/h2&gt;

&lt;p&gt;&lt;em&gt;It must be the GPU&lt;/em&gt;&lt;br&gt;
So I started as always with the performance monitor. &lt;br&gt;
The S24 averaged &lt;code&gt;110fps&lt;/code&gt; and dropped down to &lt;code&gt;~104fps&lt;/code&gt; on transition. However, the iPhone showed &lt;code&gt;~39fps&lt;/code&gt; on the UI counter, while the JS stayed at &lt;code&gt;60fps&lt;/code&gt;. &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdvc8r6qioez0w0dy5hv2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdvc8r6qioez0w0dy5hv2.png" alt="Performance Monitor" width="784" height="168"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Hindsight:&lt;/strong&gt;&lt;br&gt;
&lt;em&gt;The reading showed clean JS thread performance on iOS which should've hinted where the problem was.&lt;/em&gt; &lt;/p&gt;

&lt;p&gt;As these results showed what I was experiencing, but nothing more, I needed another way to really find out what went wrong. I tried the inspector, but I couldn't get valuable GPU data from it. &lt;br&gt;
I needed to go deeper. &lt;br&gt;
That's when I looked into native profiling as this was the first time I had profiled the GPU beyond the performance monitor and custom logging code. &lt;/p&gt;

&lt;p&gt;As I was mostly concerned with iOS, the tool for the job was &lt;strong&gt;Instruments&lt;/strong&gt;. I had never used it before so I needed some help. Fortunately, the tool was simple to understand and it's well-integrated in the iOS ecosystem. &lt;/p&gt;

&lt;p&gt;With a fresh release build, on device, I used the Metal System Trace template. I started recording for &lt;code&gt;~10 seconds&lt;/code&gt; with steady triggers. Here's the result:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fsmf68zlt891f5a4lpzam.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fsmf68zlt891f5a4lpzam.png" alt="Instruments numbers" width="800" height="471"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Encoder time was nearly empty.&lt;/strong&gt; Microsecond submits, apart 
from one frame-1 spike (the shader compile).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CPU usage was busy.&lt;/strong&gt; Something is working hard every frame. 
But what?&lt;/li&gt;
&lt;/ol&gt;
&lt;/blockquote&gt;

&lt;p&gt;I didn't understand much 😭 apart from some spikes with orange and some long operations. Two tracks stood out enough to extract data from; see the callouts above. The rest of the trace was noise to &lt;br&gt;
me. (It wasn't. But that comes later.)&lt;/p&gt;

&lt;p&gt;So instead of trusting the chart, I extracted data.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Encoder p50:    140 µs
Encoder p99:    621 µs
Encoder max:    15,790 µs  ← only frame 1 (shader compile)
&amp;gt; 1ms:         1          (just the first frame)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Still &lt;code&gt;42%&lt;/code&gt; of frames missed the &lt;code&gt;16.7ms&lt;/code&gt; budget by &lt;code&gt;~1ms&lt;/code&gt; 👀.&lt;br&gt;
(Yes, &lt;code&gt;16.7ms&lt;/code&gt;, not the &lt;code&gt;8.33ms&lt;/code&gt; from the metric guide. The animation was heavy enough that the display had already dropped out of its &lt;code&gt;120Hz&lt;/code&gt; cadence. Why that happens comes later.)&lt;/p&gt;
&lt;h3&gt;
  
  
  One caveat:
&lt;/h3&gt;

&lt;p&gt;I could not get reliable shader execution timing from Instruments, so this did not fully clear the GPU. But the measured data was pointing away from Metal submit and toward something happening earlier in the frame.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fep4ligy5i5s19wcnjtww.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fep4ligy5i5s19wcnjtww.png" alt="Frame lifecycle chart showing Metal encoder time as a tiny fraction of the  raw `17ms` endraw  frame" width="800" height="174"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The frame was late at &lt;code&gt;~17ms&lt;/code&gt;, but the Metal submit was a fraction of that at &lt;code&gt;0.14ms&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Now the question is, what was happening upstream before Skia submitted the frame?&lt;/p&gt;
&lt;h2&gt;
  
  
  The Journey Upstream
&lt;/h2&gt;

&lt;p&gt;&lt;em&gt;The GPU handoff is clean, who's eating the &lt;code&gt;17ms&lt;/code&gt;?&lt;/em&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  Testing the Upstream Theory
&lt;/h3&gt;

&lt;p&gt;The encoder submit was tiny, so the cost wasn't at the CPU → GPU data handoff. The steady &lt;code&gt;17ms&lt;/code&gt; frame intervals did not look like a random GPU spike either. &lt;/p&gt;

&lt;p&gt;That made the upstream theory worth exploring.&lt;/p&gt;

&lt;p&gt;So I added the &lt;strong&gt;Time Profiler&lt;/strong&gt; to check what was happening before Skia submitted the frame.&lt;/p&gt;
&lt;h3&gt;
  
  
  Adding Time Profiler
&lt;/h3&gt;

&lt;p&gt;I hoped this would help answer the question of: which code is running during those &lt;code&gt;17ms&lt;/code&gt;?&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Total CPU samples:    3,099 ms over 10 seconds
Main Thread:          2,234 ms  (72.1%)
JavaScript thread:       18 ms  (0.6%)   ← idle
AGX / Metal driver:   single-digit samples each
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This shows 3 unambiguous things:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Metal driver&lt;/strong&gt; is barely doing any work.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;JS thread&lt;/strong&gt; is idle.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;UI (Main Thread)&lt;/strong&gt; (where the &lt;em&gt;worklet&lt;/em&gt; lives) is doing most of the work.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;In this setup the &lt;code&gt;worklet&lt;/code&gt; runs on the UI runtime, which shows up as Main Thread work on iOS, and that's why the React Native JS thread is idle while the animation was doing heavy work.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqp3huebg7usesig1z3n1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqp3huebg7usesig1z3n1.png" alt="Time profiler crop" width="794" height="43"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And there it was: the &lt;code&gt;worklet&lt;/code&gt; looked guilty. But could it be the calculations, or something else entirely?&lt;/p&gt;

&lt;h2&gt;
  
  
  The Worklet Was the Wall
&lt;/h2&gt;

&lt;p&gt;&lt;em&gt;What made the worklet slow? Was it the math or the way I wrote it&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Now that I had narrowed down the symptom to the Main Thread, I dug in deeper and traced its call tree. &lt;/p&gt;

&lt;p&gt;Two culprits surfaced from leaf functions, &lt;code&gt;putComputedWithReceiver_RJS&lt;/code&gt; and &lt;code&gt;malloc/free (_xzm_xzone_malloc_tiny)&lt;/code&gt;. I had to search what they were as I was unfamiliar with Hermes lower-level functions.&lt;/p&gt;

&lt;p&gt;In plain English, the profiler was pointing at two things:&lt;/p&gt;

&lt;blockquote&gt;
&lt;ul&gt;
&lt;li&gt;generic indexed writes: &lt;code&gt;buffer[i] = value&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;short-lived allocations: creating and discarding a fresh array every frame&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;putComputedWithReceiver_RJS&lt;/code&gt;:&lt;/strong&gt; Hermes' bytecode handler for a computed property write, &lt;code&gt;obj[key] = value&lt;/code&gt; where the key is computed, like an array index. It's the slow path because Hermes can't use a fixed object shape; it has to do a generic lookup per write.&lt;/p&gt;

&lt;p&gt;In the code: the worklet's &lt;code&gt;buffer[i] = x, buffer[i + 1] = y&lt;/code&gt;, … loops over &lt;code&gt;128&lt;/code&gt; mostly static balls and &lt;code&gt;64&lt;/code&gt; oscillating ones.&lt;br&gt;
At 4 indexed writes per ball, that came to &lt;code&gt;~768 writes&lt;/code&gt; per frame at &lt;code&gt;192 balls&lt;/code&gt;. That loop is what lit this function up.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;malloc/free (_xzm_xzone_malloc_tiny)&lt;/code&gt;:&lt;/strong&gt; the iOS allocator's fast path for tiny allocations. &lt;br&gt;
Seeing it hot suggested is allocating short-lived objects that the GC deallocates right away.&lt;/p&gt;

&lt;p&gt;In our code: the &lt;code&gt;new Array(MAX_META_BALLS * 4).fill(0)&lt;/code&gt; allocated a fresh buffer and discarded it every frame, &lt;code&gt;120×/sec&lt;/code&gt;. That's the allocation + GC churn this pair represents.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvgdvwxeolvj1mvgwlngc.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvgdvwxeolvj1mvgwlngc.png" alt="what runs in the useFrameCallback() and the hot functions" width="800" height="541"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Both costs came from useFrameCallback(), the Reanimated callback I was using to prepare the animation state every frame: &lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Allocate a fresh buffer&lt;/li&gt;
&lt;li&gt;Compute ball positions&lt;/li&gt;
&lt;li&gt;Write values into the buffer&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;So the worklet was not losing time because the ball position math was complex. It was losing time because of how I was rebuilding and rewriting a large JS array every frame.&lt;/p&gt;

&lt;p&gt;At this point, the goal looked obvious: remove the allocation, reduce the indexed writes, and stop rebuilding the buffer every frame.&lt;/p&gt;

&lt;p&gt;I was happy. I thought I had found the bottleneck.&lt;/p&gt;

&lt;p&gt;I hadn’t.&lt;/p&gt;
&lt;h2&gt;
  
  
  The False Victory
&lt;/h2&gt;

&lt;p&gt;&lt;em&gt;Double-buffer arrays, &lt;code&gt;80fps&lt;/code&gt; → &lt;code&gt;104fps&lt;/code&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;So I had two issues to fix:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;allocation churn: allocating a new buffer every frame.&lt;/li&gt;
&lt;li&gt;indexed writes: writing many computed indexes per frame, which hit Hermes’ generic property-write path.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;
  
  
  Allocation churn
&lt;/h3&gt;

&lt;p&gt;First, I tackled the allocation churn. &lt;/p&gt;

&lt;p&gt;The solution had two parts: &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;No &lt;code&gt;new Array(...)&lt;/code&gt; allocation and no &lt;code&gt;.fill(0)&lt;/code&gt; on every frame, which ballooned the memory and pressured the GC.&lt;/li&gt;
&lt;li&gt;Allocate two reusable arrays once at module scope, fill in place, and flip which one you write each frame. &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Why two buffers, not one?&lt;/p&gt;

&lt;p&gt;Because Reanimated needs a new reference to propagate the update. If I reused one array and assigned &lt;code&gt;ballBuffer.value = buffer&lt;/code&gt; every frame, the reference stayed the same and Skia could miss the change.&lt;/p&gt;

&lt;p&gt;Two buffers fixed both issues:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;different reference → Reanimated propagates the updates.&lt;/li&gt;
&lt;li&gt;different write/read buffer → safer handoff between reading and writing.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fkk6dlixchwpx01sqnzfh.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fkk6dlixchwpx01sqnzfh.png" alt="Two buffer explanation" width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  Result:
&lt;/h3&gt;

&lt;p&gt;This fix improved frame performance from &lt;code&gt;80fps&lt;/code&gt; → &lt;code&gt;104fps&lt;/code&gt; and reduced missed frames from &lt;code&gt;42%&lt;/code&gt; → &lt;code&gt;15%&lt;/code&gt;. A big, real, measured jump.&lt;/p&gt;

&lt;p&gt;It was a real win, and it made me confident I was on the right track, which was a morale boost. But it didn't directly address the indexed-write cost.&lt;/p&gt;

&lt;p&gt;Why call it &lt;strong&gt;false victory&lt;/strong&gt;? &lt;/p&gt;

&lt;p&gt;&lt;code&gt;104&lt;/code&gt; wasn't &lt;code&gt;120&lt;/code&gt;, and I'd convinced myself I knew why. Both of those would come back to bite me.&lt;/p&gt;
&lt;h2&gt;
  
  
  The Relapse at 128 Balls
&lt;/h2&gt;

&lt;p&gt;&lt;em&gt;64 balls clean, 128 balls janky. Hermes again... right?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;So after that "victory", I reduced the ball count from &lt;code&gt;128 border&lt;/code&gt; and &lt;code&gt;64 oscillating&lt;/code&gt; to &lt;code&gt;32 balls&lt;/code&gt; each. &lt;br&gt;
I also played around with the &lt;strong&gt;Metaball&lt;/strong&gt; knobs to get the desired look, however, I was more concerned with the performance.&lt;/p&gt;

&lt;p&gt;&lt;iframe class="tweet-embed" id="tweet-2057233292941025410-684" src="https://platform.twitter.com/embed/Tweet.html?id=2057233292941025410"&gt;
&lt;/iframe&gt;

  // Detect dark theme
  var iframe = document.getElementById('tweet-2057233292941025410-684');
  if (document.body.className.includes('dark-theme')) {
    iframe.src = "https://platform.twitter.com/embed/Tweet.html?id=2057233292941025410&amp;amp;theme=dark"
  }



&lt;/p&gt;

&lt;p&gt;The performance was the best so far, holding at &lt;code&gt;~122fps&lt;/code&gt;→&lt;code&gt;123fps&lt;/code&gt; with &lt;code&gt;~0.1%&lt;/code&gt; missed frames. &lt;/p&gt;

&lt;p&gt;This was perfect, however, I couldn't get the look I wanted. &lt;br&gt;
So I increased the ball counts to &lt;code&gt;64 border&lt;/code&gt; and &lt;code&gt;64 oscillating&lt;/code&gt; and the stutter came back. &lt;/p&gt;

&lt;p&gt;Smaller this time but still real.&lt;/p&gt;

&lt;p&gt;So here's what we have so far, after the double-buffer fix: &lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Ball Count&lt;/th&gt;
&lt;th&gt;Avg. FPS&lt;/th&gt;
&lt;th&gt;Miss %&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;64 (32 + 32)&lt;/td&gt;
&lt;td&gt;~122–123fps&lt;/td&gt;
&lt;td&gt;~0.1%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;128 (64 + 64)&lt;/td&gt;
&lt;td&gt;~115fps&lt;/td&gt;
&lt;td&gt;~1.5%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;I thought, Hermes again, right!&lt;/p&gt;

&lt;p&gt;The numbers don't lie: adding more balls meant more writes, which dropped the frames. &lt;/p&gt;

&lt;p&gt;It wasn't that one-to-one though, and that's what I found out.&lt;/p&gt;

&lt;p&gt;The reflex "Hermes again" was untested. &lt;/p&gt;

&lt;p&gt;So I measured the write loop directly.&lt;/p&gt;

&lt;p&gt;I logged inside the worklet itself at the write loop, logging write-ms every &lt;code&gt;60 frames&lt;/code&gt; as a baseline and on every dropped frame, in a release build.&lt;/p&gt;

&lt;p&gt;The results were shocking with writes maxing at &lt;code&gt;0.90ms&lt;/code&gt;, even the dropped frames weren't slow they were even faster. That was the smoking gun 🔫. If the write loop finishes under &lt;em&gt;1ms&lt;/em&gt;, even on dropped frames, then the bottleneck must be something downstream. &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2ylt5j189s4o9f4rbbxs.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2ylt5j189s4o9f4rbbxs.png" alt="pipeline GPU suspect" width="800" height="248"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  One Variable Was Actually Three
&lt;/h2&gt;

&lt;p&gt;&lt;em&gt;"More balls" was never one knob&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;I kept trying ball counts, yet something felt off. &lt;/p&gt;

&lt;p&gt;I even reduced the number of writes by making some passed values static, like the ball radius.&lt;/p&gt;

&lt;p&gt;Nothing changed.&lt;/p&gt;

&lt;p&gt;It must be something else that I was missing, maybe I was looking in the wrong place.&lt;/p&gt;

&lt;p&gt;I thought I was testing one variable:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;more balls&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;But I was actually changing three things at once.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Variable&lt;/th&gt;
&lt;th&gt;What changed&lt;/th&gt;
&lt;th&gt;Why it mattered&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;JS writes&lt;/td&gt;
&lt;td&gt;More balls meant more buffer writes&lt;/td&gt;
&lt;td&gt;More Hermes indexed writes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Shader loop work&lt;/td&gt;
&lt;td&gt;More balls meant more metaball evaluations per pixel&lt;/td&gt;
&lt;td&gt;More SDF work inside the fragment shader&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pixel count&lt;/td&gt;
&lt;td&gt;Supersampling increased the rendered pixel area&lt;/td&gt;
&lt;td&gt;The shader ran over many more fragments&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The first one was obvious because I had just fixed the array allocation problem.&lt;/p&gt;

&lt;p&gt;The second one was easier to miss.&lt;/p&gt;

&lt;p&gt;Inside the shader implementation, each pixel loops over the active balls. So increasing the ball count does not just add more data to pass from the worklet. It also adds more work for every pixel the shader touches.  &lt;/p&gt;

&lt;p&gt;However, the third variable was the real trap: supersampling. &lt;/p&gt;

&lt;p&gt;I use supersampling to keep shaders clean and crisp. The idea is simple: render the shader into an offscreen layer at a higher scale, then downsample it back to the logical size for antialiasing.&lt;/p&gt;

&lt;p&gt;In code, it looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ss = device pixels × the supersample factor.&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ss&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;PixelRatio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;SUPERSAMPLE_SCALE&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;...&lt;/span&gt;
  &lt;span class="c1"&gt;// outer scale(1/ss) downsamples the ss× layer back to logical size (the AA)&lt;/span&gt;
  &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Group&lt;/span&gt; &lt;span class="nx"&gt;transform&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{[{&lt;/span&gt; &lt;span class="na"&gt;scale&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;ss&lt;/span&gt; &lt;span class="p"&gt;}]}&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="cm"&gt;/* inner scale(ss) renders the metaball field at ss× density (the cost) */&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Group&lt;/span&gt;
      &lt;span class="na"&gt;transform&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="na"&gt;scale&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ss&lt;/span&gt; &lt;span class="p"&gt;}]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
      &lt;span class="na"&gt;layer&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Paint&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
          &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;RuntimeShader&lt;/span&gt; &lt;span class="err"&gt;...&lt;/span&gt;&lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Paint&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That looks harmless, but the cost is quadratic in scale.&lt;/p&gt;

&lt;p&gt;Because scaling affects both the width and the height, the pixel count gets squared: &lt;code&gt;pixel cost ≈ ss²&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fl6h3o36wwkbd0qtl4ta0.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fl6h3o36wwkbd0qtl4ta0.png" alt="supersampling diagram" width="800" height="574"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;So on a &lt;code&gt;3×&lt;/code&gt; device, even &lt;code&gt;SUPERSAMPLE_SCALE = 1&lt;/code&gt; means the shader can run over roughly &lt;code&gt;9×&lt;/code&gt; as many pixels.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fai9tom66aj5pzq1ivf64.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fai9tom66aj5pzq1ivf64.png" alt="no vs full supersample" width="800" height="1326"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And every one of those pixels loops over the active balls.&lt;/p&gt;

&lt;p&gt;So the cost wasn't the &lt;strong&gt;ball count&lt;/strong&gt; itself.&lt;/p&gt;

&lt;p&gt;It was closer to:&lt;br&gt;
&lt;code&gt;active_balls × shaded pixels&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;And supersampling was the hidden multiplier:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;(PixelRatio × SUPERSAMPLE_SCALE)²&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;The full formula comes next.&lt;/p&gt;

&lt;p&gt;That changed the whole investigation.&lt;/p&gt;

&lt;p&gt;The relapse at &lt;code&gt;128 balls&lt;/code&gt; was not clean proof that Hermes writes were still the bottleneck.&lt;/p&gt;

&lt;p&gt;It was a mixed test, with hidden variables I was yet to discover.&lt;/p&gt;

&lt;p&gt;I had increased the writes, increased the shader loop work, and increased the amount of fragment work all at the same time.&lt;/p&gt;

&lt;p&gt;The real cost model was bigger than I thought.&lt;/p&gt;
&lt;h2&gt;
  
  
  The Real Cost Model
&lt;/h2&gt;
&lt;h3&gt;
  
  
  Formula
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;active_balls × logical_resolution × (PixelRatio × SUPERSAMPLE_SCALE)²&lt;/code&gt; &lt;/p&gt;

&lt;p&gt;This was the formula that I should've used from the start, as it separates the real costs: &lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Term&lt;/th&gt;
&lt;th&gt;Meaning&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;active_balls&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;how many balls the shader evaluates&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;logical_resolution&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;how many logical pixels the effect covers&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;(PixelRatio × SUPERSAMPLE_SCALE)²&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;how much supersampling multiplies the shader calculation&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fiudud7iy6uq7einv44y7.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fiudud7iy6uq7einv44y7.png" alt="Cost model diagram" width="800" height="600"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  The 128-Ball Example
&lt;/h3&gt;

&lt;p&gt;&lt;em&gt;27 billion ball evaluations per second&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Let's apply the formula to the &lt;code&gt;128 balls&lt;/code&gt; case:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The exact number matters less than the scale. This was not a perfect GPU cost estimate, it was a way to see the order of magnitude I was asking the shader to handle.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;code&gt;active_balls&lt;/code&gt; = &lt;code&gt;128&lt;/code&gt;&lt;br&gt;
&lt;code&gt;logical_resolution&lt;/code&gt; ≈ &lt;code&gt;402 × 874 = 351,348&lt;/code&gt;&lt;br&gt;
&lt;code&gt;(PixelRatio × SUPERSAMPLE_SCALE)²&lt;/code&gt; = &lt;code&gt;(3 × 0.75)² ≈ 5.06&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;So the estimated work per frame was:&lt;br&gt;
&lt;code&gt;128 × (351,348 × (3 × 0.75)²) ≈ 227.7 million estimated ball evaluations per frame&lt;/code&gt;&lt;br&gt;
At &lt;code&gt;120fps&lt;/code&gt;:&lt;br&gt;
&lt;code&gt;227.7 million × 120 ≈ 27.3 billion ball evaluations per second&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;That was the number that I should've been looking at from the start.&lt;/p&gt;

&lt;p&gt;Not just how many balls I had or how many values I wrote.&lt;/p&gt;

&lt;p&gt;The real cost was how many times the shader had to evaluate those balls across the rendered pixels.&lt;/p&gt;
&lt;h3&gt;
  
  
  Proving The Formula
&lt;/h3&gt;

&lt;p&gt;The formula was still just a theory. To test it, I changed exactly one variable and watched the frame time follow. Same &lt;code&gt;128 balls&lt;/code&gt;, same buffer writes, same shader, changing nothing but &lt;code&gt;SUPERSAMPLE_SCALE&lt;/code&gt;:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Config&lt;/th&gt;
&lt;th&gt;Rendered pixels&lt;/th&gt;
&lt;th&gt;Avg. FPS&lt;/th&gt;
&lt;th&gt;Miss %&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;128 @ 0.75 (ss² ≈ 5)&lt;/td&gt;
&lt;td&gt;~5× logical&lt;/td&gt;
&lt;td&gt;~115fps&lt;/td&gt;
&lt;td&gt;~1.5%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;128 @ logical (ss² = 1)&lt;/td&gt;
&lt;td&gt;1× logical&lt;/td&gt;
&lt;td&gt;~124fps&lt;/td&gt;
&lt;td&gt;~0%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Same balls. Same writes. Same shader. The only thing that changed was how many pixels the shader ran over, and the stutter tracked it exactly. Drop the pixel count and the jank vanished; the frame held a clean &lt;code&gt;120fps&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;That was the confirmation. Ball count and writes were not the whole story. After the double-buffer fix, the dominant cost was fragment shader work: evaluating those balls across every rendered pixel.&lt;/p&gt;

&lt;p&gt;That made the &lt;code&gt;~115fps&lt;/code&gt; result much less mysterious.&lt;/p&gt;

&lt;p&gt;The frame did not need one huge bottleneck to miss. It only needed a small amount of extra fragment work repeated hundreds of millions of times.&lt;/p&gt;

&lt;p&gt;After the double-buffer fix, Hermes was no longer the main suspect. The bigger cost was now fragment work.&lt;/p&gt;
&lt;h2&gt;
  
  
  The ProMotion Twist
&lt;/h2&gt;

&lt;p&gt;&lt;em&gt;&lt;code&gt;107fps&lt;/code&gt; that felt like &lt;code&gt;60&lt;/code&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Comparable fill load, yet the iPhone 16 Pro felt worse than the Samsung S24.&lt;/p&gt;

&lt;p&gt;Not because the average FPS was dramatically lower. At &lt;code&gt;0.75&lt;/code&gt;, the iPhone averaged &lt;code&gt;~107fps&lt;/code&gt;, while the S24 averaged &lt;code&gt;~115fps&lt;/code&gt;. &lt;/p&gt;

&lt;p&gt;Close. But the iPhone missed &lt;code&gt;11.2%&lt;/code&gt; of its frames against the S24's &lt;code&gt;1.5%&lt;/code&gt;, and those misses were far more visible.&lt;/p&gt;

&lt;p&gt;The key was ProMotion. On supported iPhones, the display can dynamically vary its refresh rate up to &lt;code&gt;120Hz&lt;/code&gt;. That means the target frame budget is tiny:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;120Hz = 8.33ms per frame
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F03rpzpyz0p3aa2bg7jsy.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F03rpzpyz0p3aa2bg7jsy.png" alt="promotion frame pacing downshift" width="800" height="352"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;ProMotion dynamically selects from supported presentation rates. But for this animation, the frame-time histogram showed two dominant cadences: frames near the &lt;code&gt;8.33ms&lt;/code&gt; target and a large pile-up around &lt;code&gt;16.7ms&lt;/code&gt;, the length of a &lt;code&gt;60Hz&lt;/code&gt; frame. &lt;/p&gt;

&lt;p&gt;So a modest overshoot could read as a &lt;code&gt;2×&lt;/code&gt; frame-rate collapse, not gentle degradation.&lt;/p&gt;

&lt;p&gt;At &lt;code&gt;0.75&lt;/code&gt; supersample, the same &lt;code&gt;~5×&lt;/code&gt; fill from the last section, &lt;code&gt;342 frames&lt;/code&gt; grouped around &lt;code&gt;~16.7ms&lt;/code&gt;, versus just &lt;code&gt;3 frames&lt;/code&gt; at logical resolution.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwufcaa34a1yy9y8v0xgq.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwufcaa34a1yy9y8v0xgq.png" alt="Pro-Motion iPhone Histogram 128 balls" width="680" height="566"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;That was the twist.&lt;/p&gt;

&lt;p&gt;And it's why &lt;code&gt;107fps&lt;/code&gt; was misleading. It looked like the frame pacing was flickering between frames near the &lt;code&gt;120Hz&lt;/code&gt; cadence and frames lasting roughly &lt;code&gt;16.7ms&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Most frames made the &lt;code&gt;120Hz&lt;/code&gt; deadline.&lt;/p&gt;

&lt;p&gt;But enough frames missed. And on an adaptive panel each miss could become a frame displayed for roughly twice as long. Those misses became visible as rhythm changes: smooth, smooth, smooth, hitch, hence the stutter.&lt;/p&gt;

&lt;p&gt;The S24 produced far fewer missed frames under the same configuration: &lt;code&gt;1.5%&lt;/code&gt; versus &lt;code&gt;11.2%&lt;/code&gt;. My measurements establish that difference, but not whether it came from Android’s presentation policy, GPU throughput, driver behavior, or another platform-specific factor.&lt;/p&gt;

&lt;p&gt;Know your platform’s failure mode: on a high-refresh display, average FPS can hide the problem. Frame-time distribution and missed-frame percentage reveal the rhythm your eyes actually see.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Did Not Help
&lt;/h2&gt;

&lt;p&gt;&lt;em&gt;Every fix that made sense and changed nothing&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fb5xeckzv46x6tm6e2m9u.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fb5xeckzv46x6tm6e2m9u.gif" alt="Not working GIF" width="480" height="480"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Reducing Only the Worklet Writes
&lt;/h3&gt;

&lt;p&gt;Tackling this was my first reflex after falsely diagnosing it as an upstream issue.&lt;/p&gt;

&lt;p&gt;Freezing static values and experimenting with typed arrays may have reduced or changed the write path, while per-ball SharedValues changed the data flow entirely.&lt;/p&gt;

&lt;p&gt;None of them improved frame time because the double-buffer had already made the path cheap enough.&lt;/p&gt;

&lt;h3&gt;
  
  
  Treating Ball Count as the Whole Cost
&lt;/h3&gt;

&lt;p&gt;"More balls = more cost" was one part of the equation, actually one of three. The real driver was &lt;code&gt;active_balls × logical_resolution × supersampling²&lt;/code&gt;, which is why the same &lt;code&gt;128 balls&lt;/code&gt; could be clean at logical resolution and janky at &lt;code&gt;0.75&lt;/code&gt; of the device PixelRatio.&lt;/p&gt;

&lt;h3&gt;
  
  
  Assuming the GPU Was Cleared Too Early
&lt;/h3&gt;

&lt;p&gt;Those microsecond Metal-submit times from &lt;strong&gt;the start&lt;/strong&gt;? They only cleared the CPU→GPU handoff, never the GPU's actual work. Instruments could show me the GPU was busy, it just couldn't tell me how busy, or with what. So fragment fill stayed unmeasured the entire time, and turned out to be the dominant hidden cost.&lt;/p&gt;

&lt;p&gt;Look again at that first Instruments trace: an empty encoder track sitting directly on top of a GPU track that never goes quiet. The story was right there. I just couldn't read it yet.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fr4qsacyklptqaf2y747w.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fr4qsacyklptqaf2y747w.png" alt="Empty encoder track, microsecond submits" width="800" height="109"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0n8dfkh76ju8zp11ikaq.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0n8dfkh76ju8zp11ikaq.png" alt="GPU track showing constant activity" width="800" height="61"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fat8h1ss20jgsj20upzhd.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fat8h1ss20jgsj20upzhd.png" alt="shader animation execution pipeline" width="798" height="173"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What Actually Helped
&lt;/h2&gt;

&lt;p&gt;&lt;em&gt;Two buffers, fewer pixels, and a histogram&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Reusing the Buffers
&lt;/h3&gt;

&lt;p&gt;The double-buffer approach was one of the best solutions. It introduced a more robust way of handling the data, fixed the allocation churn, and pushed the investigation downstream, where most of the remaining frame cost lived.&lt;/p&gt;

&lt;p&gt;Two module-scope arrays, filled in place and flipped each frame, killed the per-frame allocation and its GC churn. That took me from &lt;code&gt;80fps&lt;/code&gt; to &lt;code&gt;104fps&lt;/code&gt;, while missed frames dropped from &lt;code&gt;42%&lt;/code&gt; to &lt;code&gt;15%&lt;/code&gt;. &lt;/p&gt;

&lt;p&gt;This was the one CPU-side change that genuinely mattered, back before fill became the wall.&lt;/p&gt;

&lt;h3&gt;
  
  
  Reducing the Per Pixel Evaluations
&lt;/h3&gt;

&lt;p&gt;This was the MVP of all the solutions.&lt;/p&gt;

&lt;p&gt;Pixel density was the major cost delaying frames past the &lt;code&gt;8.33ms&lt;/code&gt; budget, and it scaled with the square of the device PixelRatio &lt;code&gt;(ss²)&lt;/code&gt;, so it climbed fast.&lt;/p&gt;

&lt;p&gt;It had two knobs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;SUPERSAMPLE_SCALE&lt;/code&gt;: dropping from &lt;code&gt;0.75&lt;/code&gt; to logical resolution cut the fill &lt;code&gt;~5×&lt;/code&gt; (&lt;code&gt;227.7M → 45M&lt;/code&gt; ball evaluations per frame). On the iPhone 16 Pro that took it from &lt;code&gt;107 → 119fps&lt;/code&gt;, &lt;code&gt;11.2% → 0.3%&lt;/code&gt; missed, and the &lt;code&gt;16.7ms&lt;/code&gt; pile-up collapsed from &lt;code&gt;342 → 3 frames&lt;/code&gt;. On the S24: &lt;code&gt;115 → 124fps&lt;/code&gt;, &lt;code&gt;1.5% → 0%&lt;/code&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Active balls (&lt;code&gt;128 → 64&lt;/code&gt;): the cost is a product &lt;code&gt;active_balls × rendered_pixels&lt;/code&gt;. So I could attack it from the other side too.&lt;br&gt;
Halving the active count took it from &lt;code&gt;115fps&lt;/code&gt; and &lt;code&gt;1.5%&lt;/code&gt; missed frames → &lt;code&gt;122–123fps&lt;/code&gt; and &lt;code&gt;0.1%&lt;/code&gt; missed frames, for the same reason: fewer balls means fewer evaluations at every pixel.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Both knobs trade quality for frames. Breaking that trade-off means going into the shader itself, that's at the end.&lt;/p&gt;

&lt;h3&gt;
  
  
  Measuring Frame-Time Distribution
&lt;/h3&gt;

&lt;p&gt;On paper, the two runs weren't far apart: &lt;code&gt;107fps&lt;/code&gt; vs &lt;code&gt;119fps&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The distribution told the real story:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;p90&lt;/code&gt;: &lt;code&gt;17.02ms&lt;/code&gt; versus &lt;code&gt;8.81ms&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Missed frames: &lt;code&gt;11.2%&lt;/code&gt; versus &lt;code&gt;0.3%&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That's the same pile-up ProMotion made visible, and without it, I &lt;br&gt;
would've stayed lost in the sauce, chasing an average that never &lt;br&gt;
represented a stable cadence to begin with.&lt;/p&gt;

&lt;h3&gt;
  
  
  Separating JS Cost From Fragment Cost
&lt;/h3&gt;

&lt;p&gt;Every breakthrough came from measuring the two halves independently. &lt;/p&gt;

&lt;p&gt;First, I timed the writes inside the worklet. Then I held the ball count constant while only changing the supersampling level (1, 0.75, etc.).&lt;/p&gt;

&lt;p&gt;The in-worklet timing largely ruled out the write loop as the cause of the dropped frames:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;0.34ms&lt;/code&gt; median&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;0.90ms&lt;/code&gt; max&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;0&lt;/code&gt; of &lt;code&gt;157&lt;/code&gt; dropped frames spent more than &lt;code&gt;2ms&lt;/code&gt; writing&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The writes were actually faster on the frames that stuttered.&lt;/p&gt;

&lt;p&gt;Then, with the ball count fixed, changing only the pixel count moved performance from &lt;code&gt;107fps&lt;/code&gt; to &lt;code&gt;119fps&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;That strongly implicated the fragment work.&lt;/p&gt;

&lt;p&gt;This separation revealed where the elusive dominant cost lived.&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaway
&lt;/h2&gt;

&lt;p&gt;&lt;em&gt;The bug was never where I first looked&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  The Fix That Works Can Become the Bias That Blinds You
&lt;/h3&gt;

&lt;p&gt;Twice I was confidently wrong, and both times it was the previous win talking.&lt;/p&gt;

&lt;p&gt;First: "it's the GPU".&lt;/p&gt;

&lt;p&gt;It was the worklet. &lt;/p&gt;

&lt;p&gt;Then, after the double-buffer fix: "it's Hermes again".&lt;/p&gt;

&lt;p&gt;It was fragment fill. &lt;/p&gt;

&lt;p&gt;The model that won last round is the first suspect you should distrust this round.&lt;/p&gt;

&lt;p&gt;That bias felt reasonable because the previous bottleneck had been real. But once I had reduced it, continuing to blame it stopped being evidence and became habit.&lt;/p&gt;

&lt;h3&gt;
  
  
  Isolate Your Variables Before You Trust the Result
&lt;/h3&gt;

&lt;p&gt;The whole cliff hid behind &lt;em&gt;three knobs&lt;/em&gt; that moved together:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;worklet writes&lt;/li&gt;
&lt;li&gt;active-ball shader iterations&lt;/li&gt;
&lt;li&gt;rendered pixels from supersampling&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;"More balls" looked like one variable. &lt;/p&gt;

&lt;p&gt;It was actually three. &lt;/p&gt;

&lt;p&gt;Nothing made sense until I pinned two and moved one at a time.&lt;/p&gt;

&lt;h3&gt;
  
  
  Each Phase Needs Its Own Instrument
&lt;/h3&gt;

&lt;p&gt;FPS said frames were late. &lt;/p&gt;

&lt;p&gt;Metal trace cleared the submit. &lt;/p&gt;

&lt;p&gt;Time Profiler found the thread, then the function. &lt;/p&gt;

&lt;p&gt;But to understand the write loop cost, the most reliable measurement came from timing it inside the worklet itself.&lt;/p&gt;

&lt;p&gt;Each tool answered only one part of the question.&lt;/p&gt;

&lt;p&gt;Because Instruments was Apple’s native profiler, I wanted to treat it as the single source of truth. But even a native tool is only authoritative about what it can actually measure.&lt;/p&gt;

&lt;h3&gt;
  
  
  Know Your Platform’s Failure Mode
&lt;/h3&gt;

&lt;p&gt;Under comparable settings, the iPhone produced many more missed frames than the S24, and those misses were more visible.&lt;/p&gt;

&lt;p&gt;Average FPS hid the nature of the failure. &lt;/p&gt;

&lt;p&gt;On a high-refresh display, the frame-time distribution and missed-frame percentage map closely to the stutter your eyes actually see.&lt;/p&gt;

&lt;p&gt;The bug was never where I first looked. &lt;/p&gt;

&lt;p&gt;The method is what got me there.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's Next: Attacking the Shader Itself
&lt;/h2&gt;

&lt;p&gt;The knobs I turned, fewer balls, less supersampling bought back &lt;br&gt;
the frames by trading away quality. That works, but it treats the &lt;br&gt;
symptom.&lt;/p&gt;

&lt;p&gt;The cost model says the real enemy is the product itself: &lt;br&gt;
&lt;code&gt;active_balls × rendered_pixels&lt;/code&gt;. Every ball, evaluated at every &lt;br&gt;
pixel, every frame. As long as that multiplication stands, quality &lt;br&gt;
and performance stay locked in a trade-off.&lt;/p&gt;

&lt;p&gt;So the next investigation goes into the shader itself: can that &lt;br&gt;
product be broken? I'm experimenting with a few approaches, and &lt;br&gt;
early signs say yes, but I've been confidently wrong twice in this &lt;br&gt;
post already, so I'll let the measurements speak in part two.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final Debugging Checklist
&lt;/h2&gt;

&lt;p&gt;This is the checklist I wish I had before starting. Use it if you're working on something similar. &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Setup&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Test on a real device, in a release build. Simulators lie.&lt;/li&gt;
&lt;li&gt;[ ] Commit each experiment with the config and numbers. Reconstructing performance results later is painful.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Measure before changing anything&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Record a baseline first.&lt;/li&gt;
&lt;li&gt;[ ] Do not trust average FPS alone. Capture frame-time distribution and missed-frame %. My &lt;code&gt;107fps&lt;/code&gt; average hid a &lt;code&gt;16.7ms&lt;/code&gt; pile-up.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Locate the phase&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Check JS thread vs UI thread first. Clean JS + busy UI usually means worklet or native-side work.&lt;/li&gt;
&lt;li&gt;[ ] Tiny GPU submit time does &lt;strong&gt;not&lt;/strong&gt; clear the GPU. It only clears the CPU → GPU handoff.&lt;/li&gt;
&lt;li&gt;[ ] Use Time Profiler to find hot leaf functions. Look up unfamiliar ones before building a theory.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;If the CPU side is hot&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Kill per-frame allocations. Pre-allocate buffers and reuse them.&lt;/li&gt;
&lt;li&gt;[ ] Double-buffer when the reactive system needs a new reference to propagate updates.&lt;/li&gt;
&lt;li&gt;[ ] Verify with in-worklet timing. If writes stay under &lt;code&gt;1ms&lt;/code&gt; on dropped frames, stop optimizing that path.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;If frame cost scales with pixels&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Write the cost model before guessing: &lt;code&gt;work_per_pixel × rendered_pixels&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;[ ] Treat supersampling carefully. Scaling width and height means pixel cost grows with &lt;code&gt;ss²&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;[ ] Change one variable at a time. “More balls” was not one variable, it moved writes, shader loop work, and pixel count together.&lt;/li&gt;
&lt;li&gt;[ ] If fill is the wall and you can't trade more quality, the cost structure itself is the target, that usually means shader-level changes.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;If the device has a high-refresh display&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Check the &lt;code&gt;8.33ms&lt;/code&gt; boundary, not just the average FPS.&lt;/li&gt;
&lt;li&gt;[ ] On ProMotion, a small miss past the &lt;code&gt;120Hz&lt;/code&gt; window can present like a &lt;code&gt;16.7ms&lt;/code&gt; frame, a visible &lt;code&gt;2×&lt;/code&gt; hitch.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Bias check&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;[ ] After every fix, re-measure from scratch.&lt;/li&gt;
&lt;li&gt;[ ] The last bottleneck you found is the first suspect to distrust.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>reactnative</category>
      <category>mobile</category>
      <category>performance</category>
      <category>expo</category>
    </item>
    <item>
      <title>Fibonacci grid in React</title>
      <dc:creator>Mohamed Eltohamy</dc:creator>
      <pubDate>Fri, 19 Jun 2026 10:04:22 +0000</pubDate>
      <link>https://dev.to/imcitizen13/fibonacci-grid-in-react-4poe</link>
      <guid>https://dev.to/imcitizen13/fibonacci-grid-in-react-4poe</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F59jzcftywl3j1vfh0340.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F59jzcftywl3j1vfh0340.png" alt="Leonardo Fibonacci Color and Seashell with fibonacci sequence overlay" width="799" height="363"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Goal
&lt;/h2&gt;

&lt;p&gt;I want to create a &lt;strong&gt;Fibonacci&lt;/strong&gt; grid pattern component in &lt;strong&gt;React&lt;/strong&gt; that can the user can use and add elements to in any orientation.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fbmvc752l6e94eoey2oiv.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fbmvc752l6e94eoey2oiv.png" alt="Fibonacci View" width="800" height="492"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  React Component ⚛️
&lt;/h2&gt;

&lt;p&gt;I started by creating a React Component called /code &lt;strong&gt;FibonacciGrid&lt;/strong&gt;, and then added a couple of states.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;em&gt;Orientation&lt;/em&gt; is an enum that is either &lt;strong&gt;horizontal&lt;/strong&gt; or &lt;strong&gt;vertical.&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;nBoxes&lt;/em&gt; for specifying the number of boxes/elements in the grid &lt;em&gt;(It should be a multiple of 3)&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;elementResolution&lt;/em&gt; which holds the width and height of the grid specified by the user.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fc26upfz3t2bshpgzu2cl.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fc26upfz3t2bshpgzu2cl.png" alt="Component basic code structure" width="799" height="290"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Grid vs Flex Layouts
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Grid layout&lt;/strong&gt; was my initial choice when I got started with this as it’s called &lt;strong&gt;FibonacciGrid&lt;/strong&gt; after all and a grid looks like it’ll be the obvious choice right? … well wrong because for the design that I made, I only needed to worry about 3 elements (Probably could’ve been done easily by grid but I already did something like it with &lt;em&gt;flex&lt;/em&gt; and I love ❤️ &lt;em&gt;flex&lt;/em&gt;. So flex is a go; here’s the orientation that I’m aiming for: &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fi8o5ans5oa41gt2gffay.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fi8o5ans5oa41gt2gffay.png" alt="Grid vs Flex Layouts desired view" width="800" height="479"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I’m going to nest this layout in itself as you can see here:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fujxx73bjgz4953f0i71n.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fujxx73bjgz4953f0i71n.png" alt="Recursive Fibonacci view" width="800" height="481"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You can see how it keeps nesting a new FibView, yeah I’m calling that now. Getting back to what's at hand, if the user sets the number of views/boxes to more than 3 then we’ll have to nest another view and repeat the process.&lt;/p&gt;

&lt;h2&gt;
  
  
  Design
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fb4dd3ojlqxusp9ei3kyo.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fb4dd3ojlqxusp9ei3kyo.png" alt="Initial design" width="800" height="492"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I made this &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpz4el85qx57ofls01bdi.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpz4el85qx57ofls01bdi.png" alt="Output" width="800" height="414"&gt;&lt;/a&gt;   &lt;/p&gt;

&lt;h2&gt;
  
  
  Orientation Wars
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fug75cvniqmm1h11ju70a.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fug75cvniqmm1h11ju70a.png" alt=" " width="800" height="460"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Making the view have one behavior is simple enough, but now I need to make it dynamic.&lt;/p&gt;

&lt;p&gt;I set up a lot of variables and this function changes the variables on orientation change….&lt;/p&gt;

&lt;p&gt;And it worked, for the horizontal view, but not for the Vertical (as expected 🤪).&lt;/p&gt;

&lt;p&gt;Here’s how it looked:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6vvwz26f4xs2vx1bg9dq.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6vvwz26f4xs2vx1bg9dq.png" alt="How the thing looks" width="800" height="568"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;and that's even after I applied a little bit of React Wizardry.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdnexnkcpgo5odj7dsylv.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdnexnkcpgo5odj7dsylv.png" alt="Orientation " width="799" height="388"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;if so then add it to the bottom of the flex row direction, ah yeah I did that too, now I change the &lt;em&gt;flex-direction&lt;/em&gt; depending on the orientation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Need to fix the vertical orientation issue
&lt;/h2&gt;

&lt;p&gt;OMG!!! So I was adding the 1 view to the same flex that contains the other two views, so I ended up with the 3 views with a 37% height and that was the issue.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reverse/Flip Orientations
&lt;/h2&gt;

&lt;p&gt;Vertical reverse and horizontal reverse, &lt;/p&gt;

&lt;p&gt;The end result should be like the one on the figure on the left, after changing a few things I set up the view to change the size to a fixed width and height based on its orientation.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fcw7zzut7l1g1hglbbo3c.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fcw7zzut7l1g1hglbbo3c.png" alt=" " width="800" height="460"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;For some reason, I got the following figures on the right, not the same but close. Now starts the game of trial and error.&lt;/p&gt;

&lt;h3&gt;
  
  
  HorizontalReverse
&lt;/h3&gt;

&lt;p&gt;That was an easy one, I just needed to add one line to the condition that adds the large view to the end.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{(orientation === OrientationEnum.vertical ||
        orientation === OrientationEnum.horizontalReverse) &amp;amp;&amp;amp; (
        &amp;lt;div
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;onto the next one…&lt;/p&gt;

&lt;h3&gt;
  
  
  VerticalReverse
&lt;/h3&gt;

&lt;p&gt;Now &lt;/p&gt;

&lt;h2&gt;
  
  
  Orientation Expansion
&lt;/h2&gt;

&lt;p&gt;So as I was figuring out what to do I noticed that I missed a couple of orientations 4 to be exact. I was contemplating whether to implement them now or not as they’ll add an extra level of complexity, knowing me I just had to do it. This btw was a side project of a side project soooooo… let’s get into it. &lt;/p&gt;

&lt;p&gt;I went and added the extra orientations and worked out a nifty new way to figure it all out.&lt;/p&gt;

&lt;h3&gt;
  
  
  Flip (the nifty way)
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8h5760bz0g0nglkmiady.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8h5760bz0g0nglkmiady.png" alt="Flip variable" width="799" height="385"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Now it’s getting complicated, but don’t worry I think I figured a way to simplify things.&lt;br&gt;
I added a new variable &lt;em&gt;flip&lt;/em&gt; which will just vertically flip all of the original 4 orientations, you can ask me why don’t I make flip-horizontal and flip-vertical, and to that, I say shhhh, not now but that is the future plan. (Something for another day).&lt;/p&gt;

&lt;p&gt;I leave this here as there is something more important that I need to do.&lt;/p&gt;
&lt;h2&gt;
  
  
  Important Change
&lt;/h2&gt;

&lt;p&gt;As of now most of the views are working well, I thought there should be some style added to them.&lt;/p&gt;

&lt;p&gt;I went and downloaded a couple of beautiful images from &lt;a href="https://unsplash.com/photos/a-colorful-mural-on-the-side-of-a-building-3PBKAHfEkxA?utm_content=creditCopyText&amp;amp;utm_medium=referral&amp;amp;utm_source=unsplash" rel="noopener noreferrer"&gt;Unsplash&lt;/a&gt; from these amazing artists, check their work in the links below.&lt;/p&gt;

&lt;p&gt;Photos by &lt;a href="https://unsplash.com/@freguesiadeestrela?utm_content=creditCopyText&amp;amp;utm_medium=referral&amp;amp;utm_source=unsplash" rel="noopener noreferrer"&gt;Freguesia de Estrela&lt;/a&gt; on &lt;a href="https://unsplash.com/photos/a-colorful-mural-on-the-side-of-a-building-3PBKAHfEkxA?utm_content=creditCopyText&amp;amp;utm_medium=referral&amp;amp;utm_source=unsplash" rel="noopener noreferrer"&gt;Unsplash&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Photo by &lt;a href="https://unsplash.com/@fakurian" rel="noopener noreferrer"&gt;Milad Fakurian&lt;/a&gt; on &lt;a href="https://unsplash.com/photos/a-colorful-mural-on-the-side-of-a-building-3PBKAHfEkxA?utm_content=creditCopyText&amp;amp;utm_medium=referral&amp;amp;utm_source=unsplash" rel="noopener noreferrer"&gt;Unsplash&lt;/a&gt; &lt;/p&gt;

&lt;p&gt;Photo by &lt;a href="https://unsplash.com/@danieljschwarz" rel="noopener noreferrer"&gt;Daniel J. SchwarzHire&lt;/a&gt; on &lt;/p&gt;

&lt;p&gt;Photo by &lt;a href="https://unsplash.com/@neom" rel="noopener noreferrer"&gt;NEOM&lt;/a&gt; on &lt;a href="https://unsplash.com/photos/a-colorful-mural-on-the-side-of-a-building-3PBKAHfEkxA?utm_content=creditCopyText&amp;amp;utm_medium=referral&amp;amp;utm_source=unsplash" rel="noopener noreferrer"&gt;Unsplash&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Photo by &lt;a href="https://unsplash.com/@marekpiwnicki" rel="noopener noreferrer"&gt;Marek Piwnicki&lt;/a&gt; on &lt;a href="https://unsplash.com/photos/a-colorful-mural-on-the-side-of-a-building-3PBKAHfEkxA?utm_content=creditCopyText&amp;amp;utm_medium=referral&amp;amp;utm_source=unsplash" rel="noopener noreferrer"&gt;Unsplash&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;OK, let’s address the elephant in the room (🦣&amp;nbsp;me)&amp;nbsp;not you 🐘&amp;nbsp;yeah you, seriously though we need to now add children to the FibView. I’m now faced with a choice either to implement adding children to the view which is the whole point of this or add the images in the fibview itself, hmmmm.. Well as you know by now I usually take the harder route and give up midway, but that’s what what we’re gonna do right now.&lt;/p&gt;

&lt;p&gt;So as I never did this before I jumped into React’s docs and I think it’s gonna be something like this&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;FibView&amp;gt; 
    &amp;lt;img src="1.png"&amp;gt;&amp;lt;/img&amp;gt;
    &amp;lt;img src="2.png"&amp;gt;&amp;lt;/img&amp;gt;
    &amp;lt;img src="3.png"&amp;gt;&amp;lt;/img&amp;gt;
    &amp;lt;img src="4.png"&amp;gt;&amp;lt;/img&amp;gt;
    &amp;lt;img src="5.png"&amp;gt;&amp;lt;/img&amp;gt;
&amp;lt;/FibView&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then in the view we’ll get the children just like what we do with ContextProviders, so something like this&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;SomeContextProvider(props: PropsWithChildren) {
...
    {props.children}
...
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And after a little digging, it is actually the way, you gotta love modular systems.&lt;/p&gt;

&lt;p&gt;I added the children to my view like so&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;export default function FibonacciView({
  _orientation, children
}: PropsWithChildren&amp;lt;FibonacciViewProps&amp;gt;)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;However, now I have an issue, I need to add the children to different parts of the view, then I need to use an array of children which thankfully react got us covered on that by using 'Children.toArray(children)` .&lt;/p&gt;

&lt;p&gt;After that, I styled the images to fill the view they’re assigned to, and then onto the assigning children to their views, then recursion which I’m trying not to think about right now. 🙆‍♂️&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8hvvrxv3mx5dgju6mvra.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8hvvrxv3mx5dgju6mvra.png" alt="Views with images" width="800" height="645"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The above are the 4 main orientations, I got distracted by this and didn’t do the flip. I should also have a gap setup for the view and restrict the number of children based on the total view height, but these are problems for another day and hopefully 🙏&amp;nbsp;I’ll be adding them as the days go by.&lt;/p&gt;

&lt;h2&gt;
  
  
  Back to Flip or BackFlip
&lt;/h2&gt;

&lt;p&gt;I’m sorry 🥲&amp;nbsp;&lt;/p&gt;

&lt;h3&gt;
  
  
  Flip horizontal
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fviq0qaj5cjr10sr91q1a.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fviq0qaj5cjr10sr91q1a.png" alt="Horizontal view and flip goal" width="800" height="408"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Flip horizontal Reverse
&lt;/h3&gt;

&lt;p&gt;I had to fix the Horizontal reverse as it was actually flip in the original view. To be honest, the code is getting more complicated, but my concern is not the complexity, it’s the readability of it and for that, I’m going to create a new /(Link to section)structure to further modularize the views.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fw1leywlfk6060izbihs7.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fw1leywlfk6060izbihs7.png" alt="Horizontal view and reverseflip goal" width="800" height="408"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F594dsbid7fzb2ssphg5h.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F594dsbid7fzb2ssphg5h.png" alt="Horizontal view and reverseflip with images" width="799" height="321"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Flip Vertical
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmftfqu8efu2cyz5rrzl4.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmftfqu8efu2cyz5rrzl4.png" alt="Flip vertical" width="800" height="408"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Now I’m getting the hang of it, Vertical flip works like a charm.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fsyglbo3of4iezz8udtoo.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fsyglbo3of4iezz8udtoo.png" alt="Flip vertical with images" width="800" height="556"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Flex is acting up
&lt;/h3&gt;

&lt;p&gt;I noticed a weird behavior &lt;/p&gt;

&lt;h3&gt;
  
  
  Flip Vertical Reverse
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fi7ioft6iih3eh7oh1cpd.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fi7ioft6iih3eh7oh1cpd.png" alt="Flip vertical reverse" width="800" height="408"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmtq4tsft6h4c32u4orx1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmtq4tsft6h4c32u4orx1.png" alt="Flip vertical reverse with images" width="800" height="562"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I’m so glad that everything is coming to fruition, yet I still don’t like the way it’s coded, I need to figure out a way to make it more simple to understand and use. &lt;/p&gt;

&lt;h2&gt;
  
  
  Aside
&lt;/h2&gt;

&lt;p&gt;I’m thinking of designating different parts of the view as zones and then making different views then assigning them to the appropriate zone, I still don’t know. Please let me know your thoughts &lt;/p&gt;

&lt;h2&gt;
  
  
  Weird Typescript issue
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;&lt;/code&gt;&lt;code&gt;&lt;br&gt;
{((orientation === OrientationEnum.horizontal &amp;amp;&amp;amp; flip) ||&lt;br&gt;
        orientation === OrientationEnum.horizontal ||&lt;br&gt;
&lt;/code&gt;&lt;code&gt;&lt;/code&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  OMGGGG
&lt;/h2&gt;

&lt;p&gt;I just realized that I need to write the component in Javascript.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6bal93sal3bl111muepb.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6bal93sal3bl111muepb.gif" alt="DarthVader noooo" width="200" height="200"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Putting it outThere
&lt;/h2&gt;

&lt;p&gt;I asked around to see how to create an npm react component for everyone to use and luckily I got a great answer from one pf the communities about where to look, and it was Vite.&lt;/p&gt;

&lt;p&gt;After some research and checking the reviews I found that the Vite solution might have some issues with SSR (Server Side Rendering) and embedding CSS on the server like in NextJs, now I needed a faster more efficient way. &lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.notion.so/Using-Tsup-f31bff77fec44ee5b27ab9ecc5c45529?pvs=21" rel="noopener noreferrer"&gt;Using Tsup&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Onto the Recursion
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1p18ugeov4uygn3yt11q.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1p18ugeov4uygn3yt11q.png" alt="Recursive View" width="800" height="481"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I figured I had to finish the recursion before I bundle it. &lt;/p&gt;

&lt;p&gt;I’m currently trying to figure out a way that will fix the sizing as I want to dynamically set the size of the fibView and then pass a % size recursively for the views&lt;/p&gt;

&lt;h2&gt;
  
  
  Refactoring...
&lt;/h2&gt;

&lt;p&gt;I had to refactor a couple of states within the fibView:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;&lt;/code&gt;&lt;code&gt;&lt;br&gt;
const [orientation, setOrientation] = useState&amp;lt;OrientationEnum&amp;gt;(_orientation);&lt;br&gt;
&lt;/code&gt;&lt;code&gt;&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;And now I’m using a prop to set it from the parent to be able to dynamically change it.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;&lt;/code&gt;&lt;code&gt;&lt;br&gt;
const [elementResolution, setElementResolution] = useState&amp;lt;ViewResolution&amp;gt;(&lt;br&gt;
    getViewResolution(orientation, _viewResolution)&lt;br&gt;
  );&lt;br&gt;
&lt;/code&gt;&lt;code&gt;&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;This also was another change for the same reason as it’ll help in the recursive step to be able to dynamically change the size and orientation of the view. Maybe I’ll revert to the original, but these states didn’t make sense as I just set them up and didn’t change them after that.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;&lt;/code&gt;&lt;code&gt;&lt;br&gt;
export type FibonacciViewProps = {&lt;br&gt;
  orientation: OrientationEnum;&lt;br&gt;
  _flip?: boolean;&lt;br&gt;
  _changeOrientation?: () =&amp;gt; void&lt;br&gt;
  _viewResolution: ViewResolution;&lt;br&gt;
};&lt;br&gt;
&lt;/code&gt;&lt;code&gt;&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;This is the updated view properties… for now.&lt;/p&gt;

&lt;h3&gt;
  
  
  Hotfix for height
&lt;/h3&gt;

&lt;p&gt;Height ap&lt;/p&gt;

&lt;h2&gt;
  
  
  Back to the recursion
&lt;/h2&gt;

&lt;p&gt;I’ll check if we have more than 3 views I’ll have to create a child FibView recursively.&lt;br&gt;
So for example If I add 8 views like so&lt;/p&gt;

&lt;p&gt;&lt;code&gt;&lt;/code&gt;&lt;code&gt;&lt;br&gt;
&amp;lt;FibView&amp;gt; &lt;br&gt;
    &amp;lt;img src="1.png"&amp;gt;&amp;lt;/img&amp;gt;&lt;br&gt;
    &amp;lt;img src="2.png"&amp;gt;&amp;lt;/img&amp;gt;&lt;br&gt;
    &amp;lt;img src="3.png"&amp;gt;&amp;lt;/img&amp;gt;&lt;br&gt;
    &amp;lt;img src="4.png"&amp;gt;&amp;lt;/img&amp;gt;&lt;br&gt;
    &amp;lt;img src="5.png"&amp;gt;&amp;lt;/img&amp;gt;&lt;br&gt;
    &amp;lt;img src="6.png"&amp;gt;&amp;lt;/img&amp;gt;&lt;br&gt;
    &amp;lt;img src="7.png"&amp;gt;&amp;lt;/img&amp;gt;&lt;br&gt;
    &amp;lt;img src="8.png"&amp;gt;&amp;lt;/img&amp;gt;&lt;br&gt;
&amp;lt;/FibView&amp;gt;&lt;br&gt;
&lt;/code&gt;&lt;code&gt;&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;2 should be displayed on the biggest views and then the remaining 6 images should be passed to the next FibView that's inside the smallest view which will display 2 images and have another FibView with the rest and so on. &lt;/p&gt;

&lt;p&gt;Now that I’m thinking about it I don’t know if this is the best way to do it&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmqb0qchwiexlr0wy26t5.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmqb0qchwiexlr0wy26t5.jpeg" alt="HMMMMMMMMMM" width="261" height="216"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;HMMMMMMM&lt;/p&gt;

&lt;p&gt;Now that I have gotten myself into this rabbit hole, I need to figure out an algorithm to calculate the next fibView in the recursion. I’ll take the horizontal view as a test case&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fuy1tzg09wkvy2rmk58wz.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fuy1tzg09wkvy2rmk58wz.png" alt="Horizontal Flips" width="800" height="408"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Take a step back (Working out the sizes of the views)
&lt;/h2&gt;

&lt;p&gt;Apparently, a parent’s height needs to be determined in order for the child to use percentages, so now I’ll refactor the code in a way to retrieve the actual height of an element and cascade the effect for all of the children.&lt;/p&gt;

&lt;p&gt;After a lot of refactoring, I managed to make it work somehow. &lt;br&gt;
But now I have a better idea, instead of supplying the width and height ///&lt;/p&gt;

&lt;h2&gt;
  
  
  Finally finished recursion (I think)
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4e61nbfezfohm8c6m23v.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4e61nbfezfohm8c6m23v.png" alt="Demo View" width="800" height="669"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fs5mbur4vciqrlxw1oqwb.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fs5mbur4vciqrlxw1oqwb.png" alt="End Result" width="800" height="373"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>typescript</category>
      <category>react</category>
      <category>showdev</category>
    </item>
  </channel>
</rss>
