From 534b8485b95a3ebaf7a1d323432200d283baa0c6 Mon Sep 17 00:00:00 2001 From: William Candillon Date: Mon, 19 Oct 2026 10:13:02 +0200 Subject: [PATCH] =?UTF-8?q?docs(=F0=9F=93=9A):=20say=20which=20loops=20rea?= =?UTF-8?q?ch=20the=20display's=20full=20rate=20on=20iOS?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A worklet or UI-runtime loop presents at the display's rate, 120 Hz on ProMotion, while React Native's JS-thread requestAnimationFrame runs at 60 Hz on iOS; apps from templates older than React Native 0.82 may still carry the iPhone 60 Hz cap, documented through Reanimated's guide. The example app opts in from its Podfile since react-native-test-app takes no extra plist keys from app.json. --- .../docs/content/docs/getting-started/canvas.mdx | 4 ++++ apps/docs/content/docs/integrations/worklets.mdx | 1 + apps/example/ios/Podfile | 16 +++++++++++++++- 3 files changed, 20 insertions(+), 1 deletion(-) diff --git a/apps/docs/content/docs/getting-started/canvas.mdx b/apps/docs/content/docs/getting-started/canvas.mdx index b9079eede6..763e574eff 100644 --- a/apps/docs/content/docs/getting-started/canvas.mdx +++ b/apps/docs/content/docs/getting-started/canvas.mdx @@ -60,6 +60,10 @@ context.present(); // React Native only `present()` is a React Native only method (it is not part of the Web WebGPU spec). It runs **synchronously** on the calling thread, so the frame is presented from whichever thread did the rendering. This works the same on every runtime: the main JS runtime, the UI thread, and dedicated worklet runtimes. Always call it after `submit()`, never before. +Your loop sets the frame rate. +A loop on the Reanimated UI runtime or on a worklet runtime (see [Worklets](/docs/integrations/worklets)) runs at the display's rate, 120 Hz on a ProMotion iPhone or iPad, while React Native's `requestAnimationFrame` on the JS thread runs at 60 Hz on iOS. +An app created from an Expo or React Native template before React Native 0.82 may still carry iOS's 60 Hz cap for iPhone: [Reanimated's performance guide](https://docs.swmansion.com/react-native-reanimated/docs/guides/performance/#-enable-120-fps) covers the `Info.plist` opt-in. + ## Pixel density Just like on the Web, a canvas has two independent sizes: diff --git a/apps/docs/content/docs/integrations/worklets.mdx b/apps/docs/content/docs/integrations/worklets.mdx index c3b5c97764..8cf3bd0477 100644 --- a/apps/docs/content/docs/integrations/worklets.mdx +++ b/apps/docs/content/docs/integrations/worklets.mdx @@ -5,6 +5,7 @@ description: Run WebGPU rendering off the JS thread on worklet runtimes. By default, WebGPU rendering runs on the JavaScript thread alongside React. Using [React Native Worklets](https://github.com/margelo/react-native-worklets), you can run WebGPU rendering on the UI thread or on a dedicated worklet runtime. +A worklet loop also runs at the display's rate, 120 Hz on a ProMotion iPhone or iPad, where the JS thread's `requestAnimationFrame` is capped at 60 Hz (see [Frame presentations](/docs/getting-started/canvas#frame-presentations)). There is even an experimental [Bundle Mode](https://docs.swmansion.com/react-native-worklets/docs/bundleMode/) that lets you run complex Three.js scenes on a dedicated thread. You can see an example of Bundle Mode working with Three.js [here](https://github.com/software-mansion-labs/Bundle-Mode-showcase-app). diff --git a/apps/example/ios/Podfile b/apps/example/ios/Podfile index 0d862c7867..775c4bbebc 100644 --- a/apps/example/ios/Podfile +++ b/apps/example/ios/Podfile @@ -6,10 +6,24 @@ require "#{ws_dir}/node_modules/react-native-test-app/test_app.rb" workspace 'Example.xcworkspace' +# react-native-test-app generates the app's Info.plist from its own template +# and takes no extra keys from app.json. Without this one iOS caps every +# display link at 60 Hz on iPhone, so a ProMotion iPhone would never show a +# canvas above 60 fps (see the docs, "Frame presentations"). +opt_in_promotion = ->(_installer) do + plist_path = File.expand_path('../node_modules/.generated/ios/Info.plist', __dir__) + return unless File.exist?(plist_path) + + info = Xcodeproj::Plist.read_from_path(plist_path) + info['CADisableMinimumFrameDurationOnPhone'] = true + Xcodeproj::Plist.write_to_path(info, plist_path) +end + options = { :hermes_enabled => true, :fabric_enabled => true, - :new_arch_enabled => true + :new_arch_enabled => true, + :post_install => opt_in_promotion } use_test_app! options