Memory Optimization Guide

1. Overview

As business features grow, RN pages become increasingly complex, and application memory usage increases accordingly. Memory in RN scenarios includes not only JS runtime objects, but also image decoding memory, page caching, component states, ArkUI nodes, and business-side temporary data. When an application occupies too much memory, the system may frequently perform memory recycling and reallocation, causing page lag, frame drops, and reduced background resident capability. In severe cases, the process may be terminated by the system.

To help developers optimize application memory usage, RNOH provides the following capabilities and adaptation points:

  • onMemoryLevel(): Developers can monitor system memory changes through this interface and execute corresponding memory recovery strategies under different memory pressure levels.
  • Background VM GC: Trigger VM GC when the application enters the background to release recyclable JS memory.
  • VM Parameter Tuning: Balance memory usage and runtime performance through JSVM startup parameter presets.
  • Image Resize Adaptation: Prioritize using resize decoding strategy when image size is significantly larger than the display area to reduce image decoding memory.

2. Using onMemoryLevel to Monitor Memory Changes

2.1 Principle Introduction

HarmonyOS provides the onMemoryLevel() interface to notify applications of current system memory pressure status. Following HarmonyOS best practices, memory levels are typically divided into three tiers:

  • MEMORY_LEVEL_MODERATE: System memory reaches a moderate level.
  • MEMORY_LEVEL_LOW: System memory is insufficient, unnecessary resources should be released.
  • MEMORY_LEVEL_CRITICAL: System memory is very tight, more resources should be released as soon as possible.

RNOH has integrated this capability. When an application inherits RNAbility, the framework receives system memory level changes and passes memory pressure signals to the RN runtime to assist in executing memory recovery.

For developers, the main purpose of onMemoryLevel() is to clean up business-reconstructable resources before the system truly enters extremely low memory, such as image caches, list caches, temporary states of off-screen pages, and preloaded data, thereby reducing the risk of continuous memory growth.

Note: Applications that have been frozen in the background will not receive onMemoryLevel() callbacks, so this interface is more suitable for graded recovery in foreground or runnable states.

2.2 Adaptation Method

For RNOH documentation, developers are usually more concerned about how to sense memory pressure and execute recovery strategies on the JS side. As long as the application has integrated RNAbility according to RNOH standard methods, the framework is responsible for transparently passing system onMemoryLevel() events to the JS side.

Developers can monitor memory pressure changes through AppState.addEventListener('memoryLevelChange', callback).

The memoryLevelChange callback parameter format is as follows:

{
  level: number;
  levelName: string;
}

Common values are shown in the following table:

Table 1 Memory Level Reference

level levelName Description
0 MEMORY_LEVEL_MODERATE System memory reaches moderate level
1 MEMORY_LEVEL_LOW System memory is insufficient, release unnecessary resources
2 MEMORY_LEVEL_CRITICAL System memory is very tight, release more resources immediately

Example code:

import React, {useEffect} from 'react';
import {AppState} from 'react-native';

export default function MemoryPressureDemo() {
  useEffect(() => {
    const memoryLevelSubscription = AppState.addEventListener(
      'memoryLevelChange',
      info => {
        console.log(
          '[memory-demo] memoryLevelChange level=',
          info.level,
          'levelName=',
          info.levelName,
        );
        // Clean up caches, stop preloading, release large object references, etc. as needed
      },
    );

    const memoryWarningSubscription = AppState.addEventListener(
      'memoryWarning',
      () => {
        console.log('[memory-demo] memoryWarning');
      },
    );

    return () => {
      memoryLevelSubscription.remove();
      memoryWarningSubscription.remove();
    };
  }, []);

  return null;
}

Where:

  • memoryLevelChange: Received every time the system calls onMemoryLevel().
  • memoryWarning: Only received additionally at MEMORY_LEVEL_CRITICAL.

If the current TypeScript type has not synchronized memoryLevelChange, you can temporarily monitor this event through type assertion and perform graded processing according to memory level:

  • MEMORY_LEVEL_MODERATE: Pause low-priority preloading, reduce cache writes.
  • MEMORY_LEVEL_LOW: Clean up reconstructable caches, such as image memory caches, list data caches.
  • MEMORY_LEVEL_CRITICAL: Immediately release large objects, terminate unnecessary tasks, clean up off-screen resources.

2.3 Notes

  • It is not recommended to execute long synchronous operations in the callback to avoid affecting current interactions.
  • This callback will not be received in background frozen state, so this interface cannot replace background memory recovery strategies.

3. Using Background VM GC to Optimize Application Memory

3.1 Principle Introduction

RNOH provides the enableBackgroundGC configuration option. When enabled, the framework sends a background memory pressure signal to the JS runtime when the application enters the background, triggering a GC to release JS objects that have become unreachable in the current session.

This capability is suitable for reducing JS heap usage in the background state, reducing the continuous occupation of system memory when the application is resident in the background.

3.2 Adaptation Method

Enable enableBackgroundGC when creating RN instance:

import { RNApp } from '@rnoh/react-native-openharmony';
RNApp({
  rnInstanceConfig: {
    name: 'app_name',
    createRNPackages: getRNOHPackages,
    enableBackgroundGC: true,
  },
  appKey: 'app_name',
})

If the application uses ArkTS page routing container, ensure that foreground/background lifecycle has been correctly integrated to guarantee that RN pages receive corresponding lifecycle signals when entering the background. This way enableBackgroundGC can take effect at the expected timing.

For businesses that are sensitive to background memory, this capability can also be used in conjunction with onMemoryLevel():

  • When foreground memory is tight, prioritize graded release;
  • When entering background, further clean up JS memory through background GC.

3.3 Notes

  • If the business strongly depends on large memory caches, some objects may need to be re-established when returning to foreground after entering background.
  • This capability can only recycle unreachable objects; caches that are still referenced will not be automatically released by GC.

4. Using JSVM Parameter Tuning to Optimize Application Memory

4.1 Principle Introduction

RNOH provides jsvmInitOptions in RNInstance configuration for passing startup parameters to JSVM. The framework also has the following preset values:

export const JSVM_INIT_OPTIONS_PRESET = {
  DEFAULT: [],
  LOW_MEMORY: [
    '--incremental-marking-hard-trigger=40',
    '--min-semi-space-size=1',
    '--max-semi-space-size=4',
  ],
  HIGH_PERFORMANCE: [
    '--incremental-marking-hard-trigger=80',
    '--min-semi-space-size=16',
    '--max-semi-space-size=16',
  ],
}

These parameters are mainly used to adjust JSVM's GC trigger timing and young generation space size, thereby balancing between "lower memory usage" and "higher runtime performance".

  • DEFAULT: Default configuration, suitable for general scenarios.
  • LOW_MEMORY: More suitable for low-memory devices or memory-sensitive scenarios.
  • HIGH_PERFORMANCE: More suitable for scenarios with interaction latency sensitivity and frequent object creation.

When developers use their own V8 virtual machine, they can modify the following three configuration parameters according to actual needs:

  • --min-semi-space-size: Minimum size of new generation (MB)
  • --max-semi-space-size: Maximum size of new generation (MB)
  • --incremental-marking-hard-trigger: Incremental marking trigger threshold (%)

Note: JSVM and V8 engine default virtual machine parameter settings are not finely tuned. Memory can be reduced through fine tuning. LOW_MEMORY is recommended.

4.2 Adaptation Method

If the application uses JSVM engine, preset parameters can be passed when creating RN instance:

import {
  JSVM_INIT_OPTIONS_PRESET,
  RNApp,
} from '@rnoh/react-native-openharmony';


RNApp({
  rnInstanceConfig: {
    name: 'app_name',
    createRNPackages: getRNOHPackages,
    jsvmInitOptions: JSVM_INIT_OPTIONS_PRESET.LOW_MEMORY,
  },
  appKey: 'app_name',
})

4.3 Notes

  • jsvmInitOptions only takes effect when using JSVM engine, not for Hermes.
  • If there is no clear tuning requirement, it is recommended to prioritize using framework presets rather than directly passing custom parameters.
  • Custom startup parameters need to be compatible with the current JSVM version.

5. Using Image Resize Adaptation to Optimize Application Memory

5.1 Principle Introduction

Images are typically the type of resource that most easily produces large memory usage in RN pages. When the original image size is much larger than the actual display size of the component, even if only displayed in a smaller area eventually, the decoding process may still occupy significant memory.

In React Native community documentation, Image provides the resizeMethod property for controlling the handling method when image size differs from component size:

  • auto: Framework automatically chooses resize or scale.
  • resize: Scale the image before decoding, more suitable for scenarios where "image is significantly larger than display area".
  • scale: Directly scale the image for display, usually faster, and more suitable for scenarios where image size is close to display area size.

RNOH has supported transparently passing resizeMethod property to underlying image nodes. Therefore, in thumbnail, list image, avatar image and other scenarios, developers can prioritize using resize to reduce memory usage caused by large image decoding.

5.2 Adaptation Method

For images whose size is significantly larger than the display area, it is recommended to explicitly specify resizeMethod="resize":

import { Image } from 'react-native';

<Image
  source={{ uri: imageUrl }}
  style={{ width: 120, height: 120 }}
  resizeMethod="resize"
/>

Recommend prioritizing resize in the following scenarios:

  • Long list thumbnails
  • Grid images
  • Avatar images
  • Waterfall flow, small card images
  • Content images where original size is significantly larger than display size

If images come from the server, it is recommended to also serve image resources by size to avoid downloading and decoding large images beyond actual display needs.

5.3 Notes

  • resize is more suitable for thumbnail scenarios; for images that need high-quality display or can be zoomed for preview, it is recommended to evaluate based on actual effect whether to continue using.
  • It is recommended to prioritize explicit configuration through component properties rather than relying on default behavior.

6. Other Optimization Methods

In daily development, the following methods can be combined to further optimize application memory usage:

  • Long list window clipping and virtualized rendering: In FlatList, SectionList, VirtualizedList and other long list scenarios, reduce the resident number of off-screen components through reasonable configuration of removeClippedSubviews, initialNumToRender, windowSize, getItemLayout and other properties, reducing memory residence caused by Native views and list item rendering. For scenarios with large data volume and complex Item structure, further evaluate using more efficient list solutions such as FlashList.

  • React component memoization: For list Items, complex child components, and pages with frequent repeated rendering, reduce unnecessary repeated rendering and temporary object creation through React.memo, PureComponent, useMemo, useCallback and other methods, reducing additional memory overhead in the rendering chain. During development, try to keep Props references stable to avoid memoization failure caused by anonymous functions, inline objects and other patterns.

  • Timely resource release in lifecycle: When page hides, component destroys, or instance exits, timely release no longer used resources, such as event subscriptions, timers, cache data, large object references, and temporary states. For RNOH pages, developers are recommended to combine page lifecycle and component lifecycle to clean up resources as soon as they are no longer needed, avoiding invalid resources long-term residing in memory.

  • Using Sendable for large data cross-thread passing: In Worker or concurrent task scenarios, if large object data needs to be passed between threads, it is recommended to prioritize using Sendable or ISendable supported data structures. Compared to ordinary object copy passing, Sendable is more suitable for large data sharing and cross-thread transmission scenarios, helping reduce memory overhead caused by additional copying, improving resource utilization efficiency in large data concurrent processing scenarios.