Bản chất Nút thắt Cũ của Kiến trúc Bridge và Sự Ra đời của JSI

Trong kiến trúc truyền thống của React Native, sự tương tác giữa luồng JavaScript và luồng Native luôn phụ thuộc hoàn toàn vào Bridge. Mọi lệnh gọi hàm, dữ liệu truyền tải hay phản hồi sự kiện đều phải trải qua quá trình tuần tự hóa sang chuỗi JSON tại luồng gửi, sau đó đi qua hàng đợi bất đồng bộ và được giải tuần tự hóa tại luồng nhận. Quy trình này bộc lộ nhược điểm nghiêm trọng khi ứng dụng cần xử lý các luồng dữ liệu tần suất cao như xử lý khung hình camera, cảm biến gia tốc, dữ liệu âm thanh thời gian thực hoặc tương tác cử chỉ phức tạp. Hiện tượng nghẽn cổ chai xuất hiện khi hàng đợi Bridge bị quá tải, khiến giao diện người dùng bị giật lag và tốc độ khung hình tụt giảm thảm hại.

Kiến trúc mới của React Native giải quyết triệt để vấn đề này bằng việc giới thiệu JavaScript Interface JSI. JSI là một lớp trừu tượng viết bằng C++ cho phép JavaScript Engine như Hermes hay V8 tương tác trực tiếp với các đối tượng Native mà không cần thông qua cơ chế chuyển đổi JSON. JSI biến các hàm Native thành các HostObject trong môi trường JavaScript Runtime, cho phép luồng JavaScript thực thi mã nguồn Native một cách đồng bộ hoặc bất đồng bộ với độ trễ cực thấp.

Cấu trúc TurboModule và Quy trình Hoạt động của HostObject

TurboModule là hệ thống module thế hệ mới được xây dựng trên nền tảng JSI, thay thế hoàn toàn NativeModule cổ điển. Thay vì khởi tạo toàn bộ các module khi ứng dụng bắt đầu khởi chạy gây lãng phí tài nguyên và kéo dài thời gian khởi động, TurboModule hỗ trợ cơ chế nạp lười. Ứng dụng chỉ tải module vào bộ nhớ khi JavaScript thực sự có lệnh gọi đến.

Cơ chế cốt lõi của TurboModule nằm ở việc hiện thực hóa giao diện jsi::HostObject. Khi một đối tượng C++ kế thừa jsi::HostObject, nó có thể ghi đè phương thức get và set. Khi JavaScript truy cập vào một thuộc tính hoặc phương thức của đối tượng này, engine sẽ gọi trực tiếp phương thức get của C++ trên cùng vùng nhớ thực thi. Điều này giúp loại bỏ hoàn toàn chi phí sao chép bộ nhớ và chi phí mã hóa dữ liệu.

Thiết kế TurboModule Tùy biến với Codegen và TypeScript Spec

Để đảm bảo an toàn kiểu dữ liệu xuyên suốt từ tầng JavaScript sang C++, React Native sử dụng Codegen để tự động tạo ra mã nguồn C++ boilerplate dựa trên định nghĩa kiểu dữ liệu viết bằng TypeScript.

Bước 1: Khởi tạo TypeScript Specification

Đầu tiên, lập trình viên định nghĩa cấu trúc của module thông qua một tệp khai báo chuẩn. Tệp này đóng vai trò là hợp đồng giao tiếp giữa các tầng kiến trúc.

import type { TurboModule } from 'react-native';
import { TurboModuleRegistry } from 'react-native';

export interface Spec extends TurboModule {
  multiplySync(a: number, b: number): number;
  computeHash(input: string): Promise<string>;
  emitDirectPayload(data: { id: string; timestamp: number }): boolean;
}

export default TurboModuleRegistry.getEnforcing<Spec>('CustomDataEngine');

Trong đoạn mã trên, phương thức multiplySync được định nghĩa để chạy đồng bộ hoàn toàn, trả về giá trị số thực thi tức thì. Phương thức computeHash trả về một Promise dành cho các tác vụ cần tính toán nặng chạy ở luồng phụ.

Bước 2: Hiện thực hóa Logic C++ Kế thừa JSI HostObject

Sau khi Codegen tạo ra các tệp header trừu tượng, chúng ta hiện thực hóa logic nghiệp vụ tại tầng C++. Dưới đây là cách triển khai trực tiếp một HostObject đơn giản bằng C++ để xử lý tính toán tốc độ cao.

#include <jsi/jsi.h>
#include <string>
#include <sstream>

using namespace facebook;

class CustomDataEngineBinding : public jsi::HostObject {
public:
  jsi::Value get(jsi::Runtime& runtime, const jsi::PropNameID& name) override {
    auto propName = name.utf8(runtime);

    if (propName == "multiplySync") {
      return jsi::Function::createFromHostFunction(
        runtime,
        name,
        2,
        [](jsi::Runtime& rt, const jsi::Value& thisVal, const jsi::Value* args, size_t count) -> jsi::Value {
          if (count < 2 || !args[0].isNumber() || !args[1].isNumber()) {
            throw jsi::JSError(rt, "Invalid arguments supplied to multiplySync");
          }
          double result = args[0].getNumber() * args[1].getNumber();
          return jsi::Value(result);
        }
      );
    }

    if (propName == "emitDirectPayload") {
      return jsi::Function::createFromHostFunction(
        runtime,
        name,
        1,
        [](jsi::Runtime& rt, const jsi::Value& thisVal, const jsi::Value* args, size_t count) -> jsi::Value {
          if (count < 1 || !args[0].isObject()) {
            return jsi::Value(false);
          }
          jsi::Object payload = args[0].getObject(rt);
          std::string id = payload.getProperty(rt, "id").getString(rt).utf8(rt);
          double timestamp = payload.getProperty(rt, "timestamp").getNumber();
          
          return jsi::Value(true);
        }
      );
    }

    return jsi::Value::undefined();
  }
};

Phương thức jsi::Function::createFromHostFunction tạo ra một hàm JavaScript có thể gọi trực tiếp. Khi JavaScript gọi hàm này, con trỏ hàm lambda trong C++ được kích hoạt ngay lập tức mà không phải gửi tin nhắn qua bất kỳ luồng trung gian nào.

Chiến lược Quản lý Luồng và An toàn Bộ nhớ

Mặc dù việc thực thi đồng bộ mang lại tốc độ vượt trội, lập trình viên cần hết sức thận trọng về kiến trúc luồng. Việc lạm dụng các lời gọi đồng bộ trong JSI có thể phản tác dụng nếu tác vụ Native chiếm dụng tài nguyên quá lâu.

Phân bổ Tác vụ Sang Luồng Nền

Mặc định, các hàm JSI được gọi sẽ chạy trên chính luồng JavaScript. Nếu một hàm C++ thực hiện tính toán mật mã hoặc xử lý hình ảnh kéo dài quá 16 mili giây, luồng JavaScript sẽ bị nghẽn, dẫn đến hiện tượng rớt khung hình trên màn hình. Đối với các tác vụ tiêu tốn CPU, giải pháp bắt buộc là sử dụng Thread Pool tại tầng Native và trả về một Promise cho JavaScript.

if (propName == "computeHash") {
  return jsi::Function::createFromHostFunction(
    runtime,
    name,
    1,
    [](jsi::Runtime& rt, const jsi::Value& thisVal, const jsi::Value* args, size_t count) -> jsi::Value {
      auto promiseConstructor = rt.global().getPropertyAsFunction(rt, "Promise");
      
      auto hostPromiseFunc = jsi::Function::createFromHostFunction(
        rt,
        jsi::PropNameID::forAscii(rt, "executor"),
        2,
        [](jsi::Runtime& runtime, const jsi::Value&, const jsi::Value* promiseArgs, size_t) -> jsi::Value {
          auto resolve = std::make_shared<jsi::Value>(runtime, promiseArgs[0]);
          
          std::thread([resolve, &runtime]() {
            std::string calculatedHash = "a8f5f167f44f4964e6c998dee827110c";
            resolve->asObject(runtime).asFunction(runtime).call(runtime, jsi::String::createFromUtf8(runtime, calculatedHash));
          }).detach();

          return jsi::Value::undefined();
        }
      );

      return promiseConstructor.callAsConstructor(rt, hostPromiseFunc);
    }
  );
}

Kiểm soát Vòng đời Đối tượng và Tránh Rò rỉ Bộ nhớ

Một lỗi phổ biến khi viết JSI là giữ tham chiếu jsi::Value, jsi::Object hoặc jsi::String xuyên qua các luồng mà không gắn kết với vòng đời của Runtime. Mỗi phiên bản jsi::Runtime sở hữu trình dọn rác riêng. Nếu lập trình viên giải phóng Runtime trong khi một luồng Native nền vẫn đang cố gắng ghi dữ liệu vào một jsi::Value, ứng dụng sẽ lập tức phát sinh lỗi truy cập bộ nhớ bất hợp pháp.

Nguyên tắc thiết kế an toàn là luôn giải nén dữ liệu từ các đối tượng JSI thành các kiểu dữ liệu nguyên bản của C++ như std::string, double hoặc cấu trúc dữ liệu thuần túy trước khi chuyển tiếp vào các luồng xử lý nền.

Sử dụng TurboModule Tùy biến trong Tầng Giao diện React Native

Sau khi hoàn tất việc đăng ký module vào hệ thống TurboModule của ứng dụng, tầng TypeScript có thể nhập và sử dụng module với trải nghiệm hoàn toàn tự nhiên, được kiểm tra kiểu chặt chẽ trong suốt quá trình viết code.

import React, { useState } from 'react';
import { View, Text, TouchableOpacity, StyleSheet } from 'react-native';
import CustomDataEngine from './specs/NativeCustomDataEngine';

export const PerformanceBenchmarkView: React.FC = () => {
  const [calculationTime, setCalculationTime] = useState<number>(0);
  const [result, setResult] = useState<number>(0);

  const handleExecuteSyncCalculation = () => {
    const startTime = performance.now();
    let computedValue = 0;
    
    for (let i = 0; i < 100000; i++) {
      computedValue = CustomDataEngine.multiplySync(i, 2);
    }

    const endTime = performance.now();
    setCalculationTime(endTime - startTime);
    setResult(computedValue);
  };

  return (
    <View style={styles.container}>
      <Text style={styles.headerText}>JSI Direct Execution Test</Text>
      <Text style={styles.infoText}>Result: {result}</Text>
      <Text style={styles.infoText}>Duration: {calculationTime.toFixed(2)} ms</Text>
      <TouchableOpacity style={styles.button} onPress={handleExecuteSyncCalculation}>
        <Text style={styles.buttonText}>Run Benchmark</Text>
      </TouchableOpacity>
    </View>
  );
};

const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
    padding: 16,
  },
  headerText: {
    fontSize: 18,
    fontWeight: 'bold',
    marginBottom: 12,
  },
  infoText: {
    fontSize: 14,
    marginBottom: 8,
  },
  button: {
    backgroundColor: '#0284c7',
    paddingHorizontal: 20,
    paddingVertical: 10,
    borderRadius: 8,
    marginTop: 16,
  },
  buttonText: {
    color: '#ffffff',
    fontWeight: '600',
  },
});

Trong kịch bản trên, việc thực hiện 100,000 phép tính qua Bridge cũ sẽ khiến luồng UI bị đơ cứng hàng giây do chi phí chuyển đổi chuỗi JSON lặp đi lặp lại. Ngược lại, thông qua JSI HostObject của TurboModule, toàn bộ vòng lặp diễn ra chỉ trong vài mili giây mà không hề gây ảnh hưởng đến tính mượt mà của giao diện.

So sánh Hiệu năng Giữa NativeModule Cũ và TurboModule JSI

  • Thời gian khởi tạo module: NativeModule cũ tiêu tốn bộ nhớ ngay khi app khởi động vì phải tải danh sách cấu hình của toàn bộ module. TurboModule chỉ khởi tạo khi xuất hiện lệnh gọi đầu tiên, giúp giảm kích thước bộ nhớ tĩnh đáng kể.
  • Băng thông truyền dữ liệu: NativeModule cũ bị giới hạn bởi tốc độ tuần tự hóa chuỗi văn bản. TurboModule JSI truyền dữ liệu bằng con trỏ bộ nhớ trực tiếp, đạt tốc độ tiệm cận với ứng dụng viết bằng ngôn ngữ thuần bản địa.
  • Cơ chế đồng bộ hóa: NativeModule chỉ hỗ trợ gọi bất đồng bộ thông qua callback hoặc promise, gây khó khăn cho các tác vụ cần kết quả ngay lập tức như tính toán bố cục layout tùy biến. TurboModule JSI cho phép lập trình viên tự do lựa chọn giữa thực thi đồng bộ hoặc bất đồng bộ.

Tổng kết

Làm chủ kỹ thuật thiết kế TurboModule kết hợp với C++ JSI là bước chuyển mình quan trọng để nâng tầm ứng dụng React Native lên chuẩn doanh nghiệp, đáp ứng những yêu cầu khắt khe nhất về hiệu năng xử lý dữ liệu và trải nghiệm người dùng. Để trang bị đầy đủ tư duy kiến trúc và làm chủ toàn bộ quy trình phát triển ứng dụng di động đa nền tảng hiện đại, bạn có thể Tham khảo khóa học "Lập trình App Mobile với React Native + TypeScript" tại đây.