Mobile Development 15 min read

How We Built a High‑Performance Canvas Chart Engine for React Native Apps

This article details Guangfa Securities' journey of using React Native to render high‑frequency stock charts, comparing WebView and native Canvas solutions, describing the custom Canvas component implementation, performance testing results, and future improvements for mobile charting.

GF Securities FinTech
GF Securities FinTech
GF Securities FinTech
How We Built a High‑Performance Canvas Chart Engine for React Native Apps

Background

Guangfa Securities was one of the earliest large‑scale adopters of React Native in the financial industry, starting in 2016 and now using it in over 30 systems and tools. In securities‑trading apps, charts such as K‑line, analysis graphs, and fund trend lines are essential, prompting continuous exploration of high‑performance rendering across browser, hybrid, and native environments.

React Native Chart Technology Background

React Native only provides the basic ART component, which is insufficient for complex charts. Two common approaches are used:

WebView : Wrap a mature web chart library (e.g., ECharts, HighCharts) in a WebView. Simple to use but suffers from performance issues.

Native Component : Either let JavaScript drive the drawing logic while native components render, or implement the entire drawing logic natively. The former offers better performance than WebView but lacks mature cross‑platform solutions; the latter delivers the best performance but requires separate codebases for iOS and Android, increasing maintenance cost.

Actual Solution Adopted

For high‑frequency, interactive K‑line charts, a pure native rendering solution was chosen. The native component, developed and maintained by the client team, has withstood millions of daily active users across all trading apps. For less critical charts (e.g., fund line charts, asset‑allocation pie charts), the low‑cost WebView + ECharts approach is used to meet tight deadlines.

Performance Optimization

WebView suffers from slow first‑load speed and high memory usage, leading the team to phase it out. Open‑source options such as react-native-charts-wrapper and victory-native were evaluated but rejected due to platform UI differences, incomplete documentation, and integration bugs. Consequently, a custom solution was pursued.

Cross‑Platform Drawing

Web drawing relies on Canvas (imperative) or SVG (declarative). The team attempted to replace the WebView’s SVG layer with ART or react-native-svg, but the effort proved costly. Canvas was chosen because it aligns with the existing ECharts implementation for WeChat Mini‑Programs and can be re‑used with minimal changes. Existing React Native Canvas libraries either depend on WebView or are not mature, so a native Canvas component was built from scratch.

React Native Canvas Component

After evaluating iOS and Android graphics APIs, the team implemented a Canvas component using Core Graphics on iOS and the Android graphics Canvas package. The component mimics the Mini‑Program draw method, allowing ECharts and F2 to run unchanged.

Working Principle

JavaScript calls Canvas API methods, which are transformed into drawing commands. These commands are sent via the React Native bridge to the native side, where reflection invokes the corresponding native Canvas methods, updating the view.

Batch Execution of Drawing Commands

Instead of drawing immediately after each API call, commands are collected asynchronously and sent in batches. The draw method triggers the transmission of the entire command set.

draw(clear = true) {
    this.drawing(clear ? 1 : 0);
    ...
}

drawing(clear) {
  if (this._isAsync) {
    CanvasNativeAPI.drawAsync(this._canvasId, this.actions, clear);
  } else {
    CanvasNativeAPI.drawSync(this._canvasId, this.actions, clear);
  }
}

iOS batch execution:

- (void)runActions {
    for (NSDictionary *action in _actions) {
        [delegate invoke:_context method:[action objectForKey:@"method"] arguments:[action objectForKey:@"arguments"]];
    }
}

High‑Performance Reflection Techniques

Because drawing commands are invoked via reflection, the team caches method signatures and invocation objects during module initialization or first use, reducing the overhead of repeated reflection.

- (void)buildInvocation {
    SEL selector = NSSelectorFromString(_method);
    NSMethodSignature *methodSignature = [_moduleClass instanceMethodSignatureForSelector:selector];
    _argumentsNumber = methodSignature.numberOfArguments - 2;
    _invocation = [NSInvocation invocationWithMethodSignature:methodSignature];
    _invocation.selector = selector;
}

- (void)invoke:(id)instance arguments:(NSArray *)arguments {
    if (_invocation == nil) {
        [self buildInvocation];
    }
    ...
}

Android stores the method reference at module initialization and invokes it directly:

public void invoke(Object moduleClassInstance, Object[] parameters) {
    Object[] arguments = new Object[parameters.length];
    if (mArgumentExtractors == null) {
        throw new Error("processArguments failed");
    }
    for (int i = 0; i < mArgumentExtractors.length; i++) {
        arguments[i] = mArgumentExtractors[i].extractArgument(parameters, i);
    }
    try {
        mMethod.invoke(moduleClassInstance, arguments);
    } catch (Exception e) {
        throw new RuntimeException("Could not invoke " + mMethodName, e);
    }
}

Synchronous Method Calls

Although React Native modules are typically asynchronous, the team exposed synchronous methods for critical operations such as measureText and batch command submission, improving real‑time interaction.

RCT_EXPORT_SYNCHRONOUS_TYPED_METHOD(id, drawSync
                                    : (NSString *)tag actions
                                    : (id)actions
                                    : (int)clear) {
    [CanvasAPI draw:tag actions:actions clear:clear];
    return @1;
}
@ReactMethod(isBlockingSynchronousMethod = true)
public Integer drawSync(final String tag, ReadableArray actions, final Integer clear) {
    draw(tag, actions, clear);
    return 1;
}

Performance Comparison

Experiments loaded identical ECharts configurations in both WebView and Canvas components on Android and iOS, measuring first‑load speed, memory usage, FPS, and CPU utilization.

Results show Canvas loads instantly, while WebView exhibits noticeable delay on the first launch. Memory consumption is lower for Canvas, FPS remains high, and CPU usage is comparable or lower (Android) and higher (iOS due to software rendering).

Android WebView : Slow load, high memory, high FPS, relatively high CPU.

Android Canvas : Normal load, moderate memory, high FPS, moderate CPU.

iOS WebView : Slow load, high memory, high FPS, high CPU.

iOS Canvas : Normal load, moderate memory, high FPS, high CPU (software rendering).

Shortcomings and Future Improvements

Missing APIs : Methods such as createImageData, getImageData, putImageData, isPointInPath, createLinearGradient, etc., are not yet implemented.

iOS CPU Usage : The current Core Graphics implementation is software‑based, leading to higher CPU usage; future work may explore GPU‑based rendering (OpenGL, Metal).

Conclusion

By implementing a native Canvas API, the team enabled the use of mature web chart libraries like ECharts within React Native, gradually replacing WebView‑based solutions and extending the approach to more drawing and animation scenarios.

CanvasReact Nativechart rendering
GF Securities FinTech
Written by

GF Securities FinTech

Dedicated to sharing the hottest FinTech practices

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.