Memory leak: `WorkletRuntime` Hermes contexts persist after host component unmounts (1128 contexts × 141 MB after typical session)
Nadie ha tomado este issue todavía.
Evaluación
- Dificultad
- 5/5
- Tiempo estimado
- Más de una semana
- Aptitud para principiantes
- 35/100
- Tipo de issue
- Error
- Claridad
- Bastante claro
- Estado de actividad
- Tranquilo
- Stack tecnológico
- cpp, react
- Área
- mobile-dev, performance
Línea de trabajo
Start by tracing WorkletRuntime::WorkletRuntime, legacyModeInit, and WorkletsModule installTurboModule using the minimal mount/unmount reproduction. Confirm the persistent allocations with Instruments and compare generations. Done means contexts created by transient worklet owners are released after unmount or scope exit without affecting the JS runtime.
Escrito por el modelo de indexación a partir del texto del issue.
Descripción
Closed:
Mistakenly posted in the wrong repo.
Correct post here: https://github.com/software-mansion/react-native-reanimated/issues/9438
Summary
In a real-world React Native app using react-native-worklets 0.8.0 (via react-native-reanimated 4.3.0 and @shopify/react-native-skia 2.5.3), Hermes runtime contexts created by WorkletRuntime are not released when their owning component unmounts. They accumulate across mount/unmount cycles. After ~50–100 cycles in our tile-based PDF viewer (each cycle = navigate to a screen with a <Canvas>, navigate back), Instruments shows 1,128 persistent hermes::BacktrackingBumpPtrAllocator instances totaling 141 MB — all attributed to worklets::WorkletRuntime::WorkletRuntime / [WorkletsModule installTurboModule].
The 1,128 count corresponds 1:1 with the number of worklet contexts created during the session. Memory grows monotonically; nothing in our explicit cleanup paths (component unmount, navigation away, Reanimated.cancelAnimation) reduces it. The contexts persist for the lifetime of the JS runtime.
Environment
react-native-worklets: 0.8.0react-native-reanimated: 4.3.0@shopify/react-native-skia: 2.5.3react-native: 0.85iOS: 17+, tested on physical device (iPhone) and simulator- Hermes engine, new architecture (Fabric)
Repro
In our app, every navigation to a screen containing <Canvas> from @shopify/react-native-skia adds ~10–60 persistent WorkletRuntime instances (depending on how many <Picture> / useSharedValue / useDerivedValue hooks the screen registers). On unmount, none of those instances are released.
A minimal repro:
import {Canvas, Picture, useSharedValue} from '@shopify/react-native-skia';
import {useEffect, useState} from 'react';
function CanvasScreen() {
// Each useSharedValue likely registers a worklet mapper
const opacity = useSharedValue(1);
return (
<Canvas style={{flex: 1}}>
<Picture picture={null} />
</Canvas>
);
}
function App() {
const [mounted, setMounted] = useState(true);
useEffect(() => {
const id = setInterval(() => setMounted(m => !m), 1000);
return () => clearInterval(id);
}, []);
return mounted ? <CanvasScreen /> : null;
}
Run the app for ~2 minutes, then attach Instruments → Allocations with "Record Call Stacks" enabled → mark generation → mount/unmount cycles → mark another generation. The WorkletRuntime instances created between generations remain in the persistent set.
Expected behavior
When the React component that owns a WorkletRuntime (or the JS function that called Worklets.createNodeRuntime / equivalent) unmounts / goes out of scope, the underlying Hermes runtime context should be released and its memory returned.
Actual behavior
WorkletRuntime instances and their Hermes runtime contexts persist for the lifetime of the JS runtime. Memory grows linearly with the number of mount/unmount cycles.
Evidence
Screenshot 1 — Statistics view (Allocations instrument), persistent allocations sorted by Bytes Used. Establishes the impact at a glance: total app heap & anonymous VM at 551 MiB persistent after the test session, with no single category accounting for it directly (the leak is spread across thousands of small Hermes allocations rolled up under Malloc 256.00 KiB, Malloc 64.00 KiB, etc.).
Screenshot 2 — Call Trees view (same Allocations recording, inverted), showing the call chain from hermes::BacktrackingBumpPtrAllocator() up through worklets::WorkletRuntime::WorkletRuntime() → [WorkletsModule installTurboModule]. This is the smoking gun: 141.03 MB / 1128 persistent instances at the leaked-allocator row, with the Hermes / worklets call stack reaching all the way down to the framework entry point.
The two relevant call-tree branches in our profile:
141.03 MB / 1128 — hermes::BacktrackingBumpPtrAllocator()
hermes::Context::Context(... hermes::OptimizationSettings, ...)
void std::__1::allocator<hermes::Context>::construct(...)
hermes::hbc::BCProviderFromSrc::create(...)
hermes::hbc::createBCProviderFromSrc(...)
138.53 MB / 1108 — hoost_make_fcontext (boost::context fibers)
void hermes::vm::evalInEnvironment(...)
std::__1::__invoke_result<...StackExecutor::StackExecutor(...) lambda...
boost::context::detail::fiber_record<boost::context::fiber...
2.50 MB / 20 — facebook::hermes::HermesRuntimeImpl::prepareJavaScriptWithSourceMap()
facebook::hermes::HermesRuntimeImpl::evaluateJavaScript(...)
facebook::jsi::RuntimeDecorator<...>::evaluateJavaScript(...)
worklets::WorkletRuntime::legacyModeInit(...) ← user-app code
worklets::WorkletRuntime::WorkletRuntime(...) ← user-app code
worklets::WorkletsModuleProxy::WorkletsModuleProxy(...)
-[WorkletsModule installTurboModule]
The third branch's leaf-count of 20 (vs 1108 / 1128 above) is interesting: it suggests installTurboModule itself runs a small fixed number of times, but each invocation triggers a downstream Context / BumpPtrAllocator chain that's amplified ~50× — possibly one runtime per worklet registered, not one per TurboModule install.
Application impact
Our app is a field-inspection tool where users open ~100 PDF blueprint drawings per day, often without restarting the app for several days between sessions. At the observed leak rate (~5 MB per drawing open under typical usage), this is multi-GB territory within a normal work-week → guaranteed OOM on iPhone, severe pressure on Android.
We've worked around this by hoisting our <Canvas> to the React tree root so it never unmounts (singleton pattern), but this required significant refactoring and isn't an option for apps that genuinely need transient Canvas mounts (e.g. modal dialogs, lazy-loaded routes).
A framework-level fix removing the cycle-on-unmount would let applications use the worklet APIs naturally without resorting to manual lifecycle workarounds.
Related
- Same symptom family, no allocator-level diagnosis: https://github.com/Shopify/react-native-skia/discussions/2675
- Adjacent (
useSkiaFrameProcessoriOS leak): https://github.com/Shopify/react-native-skia/issues/2964 - Earlier Skia-side Canvas-remount fix that did NOT address this layer: https://github.com/Shopify/react-native-skia/pull/2095
- Lenguaje dominante
- C++
- Estrellas
- 791
- Forks
- 65
- Métricas de merge de PR
- Sin PR fusionados en 30 d
Guía de contribución
Primeros pasos
- Lee el issue completo y luego la guía de contribución del proyecto.
- Comenta en el issue que vas a ocuparte — evita que dos personas hagan lo mismo.
- Haz un fork del repositorio y trabaja en una rama.
- Abre un pull request que haga referencia al número del issue.
Más de margelo/react-native-worklets-core
-
Dificultad 4/5 3-5 días Aptitud para principiantes 35/100
margelo/react-native-worklets-core#269 · 2 reacciones ·
-
Unable to download JS bundle Abierto
Dificultad 3/5 1-2 días Aptitud para principiantes 35/100
margelo/react-native-worklets-core#268 · 2 comentarios ·
-
Dificultad 3/5 1-2 días Aptitud para principiantes 72/100
margelo/react-native-worklets-core#267 · 1 comentario · 1 reacción ·
-
Dificultad 3/5 1-2 días Aptitud para principiantes 45/100
margelo/react-native-worklets-core#266 · 1 comentario · 2 reacciones ·
-
Dificultad 2/5 1-3 horas Aptitud para principiantes 20/100
Todos los issues de margelo/react-native-worklets-core
Issues similares
-
Dificultad 2/5 1-3 horas Aptitud para principiantes 75/100
-
good first issue
Dificultad 2/5 1-3 horas Aptitud para principiantes 75/100
ros2/message_filters#338 ·
-
Dificultad 2/5 1-3 horas Aptitud para principiantes 70/100
subsurface/subsurface#4984 ·
-
Dificultad 2/5 1-3 horas Aptitud para principiantes 75/100
flutter-webrtc/flutter-webrtc#2206 ·
-
litertlm-android AAR ships no consumer ProGuard rules → "mid == null" SIGABRT in minified apps Abierto
Dificultad 2/5 1-3 horas Aptitud para principiantes 70/100
google-ai-edge/LiteRT-LM#3739 ·